Files
litellm/tests/test_litellm/test_video_generation.py
T
2026-03-16 17:56:21 +05:30

2168 lines
82 KiB
Python

import asyncio
import io
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(
0, os.path.abspath("../..")
) # Adds the parent directory to the system path
import litellm
from litellm.cost_calculator import default_video_cost_calculator
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
from litellm.llms.gemini.videos.transformation import GeminiVideoConfig
from litellm.llms.openai.videos.transformation import OpenAIVideoConfig
from litellm.types.videos.main import VideoObject, VideoResponse
from litellm.videos import main as videos_main
from litellm.videos.main import (
avideo_generation,
avideo_status,
video_generation,
video_status,
)
class TestVideoGeneration:
"""Test suite for video generation functionality."""
def test_video_generation_basic(self):
"""Test basic video generation functionality."""
# Use mock_response parameter for reliable testing
response = video_generation(
prompt="Show them running around the room",
model="sora-2",
seconds="8",
size="720x1280",
mock_response={
"id": "video_123",
"object": "video",
"status": "queued",
"created_at": 1712697600,
"model": "sora-2",
"size": "720x1280",
"seconds": "8"
}
)
assert isinstance(response, VideoObject)
assert response.id == "video_123"
assert response.status == "queued"
assert response.model == "sora-2"
assert response.size == "720x1280"
assert response.seconds == "8"
def test_video_generation_with_mock_response(self):
"""Test video generation with mock response."""
mock_data = {
"id": "video_456",
"object": "video",
"status": "completed",
"created_at": 1712697600,
"completed_at": 1712697660,
"model": "sora-2",
"size": "1280x720",
"seconds": "10"
}
response = video_generation(
prompt="A beautiful sunset over the ocean",
model="sora-2",
seconds="10",
size="1280x720",
mock_response=mock_data
)
assert isinstance(response, VideoObject)
assert response.id == "video_456"
assert response.status == "completed"
assert response.model == "sora-2"
assert response.size == "1280x720"
assert response.seconds == "10"
def test_video_generation_async(self):
"""Test async video generation functionality."""
mock_response = VideoObject(
id="video_async_123",
object="video",
status="processing",
created_at=1712697600,
model="sora-2",
progress=50
)
# Mock the async_video_generation_handler to return the mock_response
async_mock = AsyncMock(return_value=mock_response)
with patch.object(videos_main.base_llm_http_handler, 'async_video_generation_handler', async_mock):
with patch.object(videos_main.base_llm_http_handler, 'video_generation_handler', side_effect=lambda **kwargs: async_mock(**kwargs)):
import asyncio
async def test_async():
response = await avideo_generation(
prompt="A cat playing with a ball",
model="sora-2",
seconds="5",
size="720x1280"
)
return response
response = asyncio.run(test_async())
assert isinstance(response, VideoObject)
assert response.id == "video_async_123"
assert response.status == "processing"
assert response.progress == 50
def test_video_generation_parameter_validation(self):
"""Test video generation parameter validation."""
# Test with minimal required parameters
response = video_generation(
prompt="Test video",
model="sora-2",
mock_response={"id": "test", "object": "video", "status": "queued", "created_at": 1712697600}
)
assert isinstance(response, VideoObject)
assert response.id == "test"
def test_video_generation_error_handling(self):
"""Test video generation error handling."""
with patch.object(videos_main.base_llm_http_handler, 'video_generation_handler', side_effect=Exception("API Error")):
with pytest.raises(Exception):
video_generation(
prompt="Test video",
model="sora-2"
)
def test_video_generation_provider_config(self):
"""Test video generation provider configuration."""
config = OpenAIVideoConfig()
# Test supported parameters
supported_params = config.get_supported_openai_params("sora-2")
assert "prompt" in supported_params
assert "model" in supported_params
assert "seconds" in supported_params
assert "size" in supported_params
def test_video_generation_request_transformation(self):
"""Test video generation request transformation."""
config = OpenAIVideoConfig()
# Test request transformation
data, files, returned_api_base = config.transform_video_create_request(
model="sora-2",
prompt="Test video prompt",
api_base="https://api.openai.com/v1/videos",
video_create_optional_request_params={
"seconds": "8",
"size": "720x1280"
},
litellm_params=MagicMock(),
headers={}
)
assert data["model"] == "sora-2"
assert data["prompt"] == "Test video prompt"
assert data["seconds"] == "8"
assert data["size"] == "720x1280"
assert files == []
assert returned_api_base == "https://api.openai.com/v1/videos"
def test_video_generation_request_decodes_encoded_character_ids(self):
"""Encoded character IDs should be decoded before upstream create-video call."""
from litellm.types.videos.utils import encode_character_id_with_provider
config = OpenAIVideoConfig()
encoded_character_id = encode_character_id_with_provider(
character_id="char_123",
provider="openai",
model_id="sora-2",
)
data, files, returned_api_base = config.transform_video_create_request(
model="sora-2",
prompt="Test video prompt",
api_base="https://api.openai.com/v1/videos",
video_create_optional_request_params={
"seconds": "8",
"size": "720x1280",
"characters": [{"id": encoded_character_id}],
},
litellm_params=MagicMock(),
headers={},
)
assert data["characters"] == [{"id": "char_123"}]
assert files == []
assert returned_api_base == "https://api.openai.com/v1/videos"
def test_video_generation_response_transformation(self):
"""Test video generation response transformation."""
config = OpenAIVideoConfig()
# Mock HTTP response
mock_http_response = MagicMock()
mock_http_response.json.return_value = {
"id": "video_789",
"object": "video",
"status": "completed",
"created_at": 1712697600,
"model": "sora-2",
"size": "1280x720",
"seconds": "12"
}
response = config.transform_video_create_response(
model="sora-2",
raw_response=mock_http_response,
logging_obj=MagicMock()
)
assert isinstance(response, VideoObject)
assert response.id == "video_789"
assert response.status == "completed"
assert response.model == "sora-2"
def test_video_generation_cost_calculation(self):
"""Test video generation cost calculation."""
import json
import os
# Try to load the local model cost map, skip if not found
cost_map_path = "model_prices_and_context_window.json"
if not os.path.exists(cost_map_path):
# Try alternative paths
alt_paths = [
os.path.join(os.path.dirname(__file__), "..", "..", cost_map_path),
os.path.join(os.path.dirname(__file__), "..", "..", "..", cost_map_path),
]
for path in alt_paths:
if os.path.exists(path):
cost_map_path = path
break
else:
pytest.skip("model_prices_and_context_window.json not found")
with open(cost_map_path, "r") as f:
litellm.model_cost = json.load(f)
# Test with sora-2 model
cost = default_video_cost_calculator(
model="openai/sora-2",
duration_seconds=10.0,
custom_llm_provider="openai"
)
# Should calculate cost based on duration (10 seconds * $0.10 per second = $1.00)
assert cost == 1.0
def test_video_generation_cost_calculation_unknown_model(self):
"""Test video generation cost calculation for unknown model."""
with pytest.raises(Exception, match="Model not found in cost map"):
default_video_cost_calculator(
model="unknown-model",
duration_seconds=5.0,
custom_llm_provider="openai"
)
def test_video_generation_cost_with_custom_model_info(self):
"""Test that custom model_info pricing is applied for video generation.
When a deployment has custom pricing via model_info, it should be used
instead of looking up the global litellm.model_cost map.
Related: https://github.com/BerriAI/litellm/issues/21907
"""
model_info = {
"output_cost_per_video_per_second": 0.05,
}
cost = default_video_cost_calculator(
model="my-custom-video-model",
duration_seconds=10.0,
model_info=model_info,
)
assert cost == 0.5
def test_video_generation_cost_custom_model_info_fallback_to_per_second(self):
"""Test that output_cost_per_second is used as fallback when
output_cost_per_video_per_second is not set in custom model_info.
Related: https://github.com/BerriAI/litellm/issues/21907
"""
model_info = {
"output_cost_per_second": 0.10,
}
cost = default_video_cost_calculator(
model="my-custom-video-model",
duration_seconds=5.0,
model_info=model_info,
)
assert cost == 0.5
def test_video_generation_cost_custom_pricing_through_completion_cost(self):
"""Test that custom video pricing flows through completion_cost via litellm_logging_obj.
This tests the full cost calculation path: completion_cost extracts model_info
from litellm_logging_obj.litellm_params.metadata.model_info and passes it to
the video cost calculator.
Related: https://github.com/BerriAI/litellm/issues/21907
"""
from litellm.cost_calculator import completion_cost
# Create mock response with usage containing duration_seconds
mock_response = MagicMock()
mock_response.usage = MagicMock()
mock_response.usage.duration_seconds = 10.0
type(mock_response)._hidden_params = {}
# Create mock litellm_logging_obj with custom pricing
mock_logging_obj = MagicMock()
mock_logging_obj.litellm_params = {
"metadata": {
"model_info": {
"output_cost_per_video_per_second": 0.05,
}
}
}
cost = completion_cost(
completion_response=mock_response,
model="openai/hunyuanvideo",
call_type="create_video",
custom_llm_provider="openai",
custom_pricing=True,
litellm_logging_obj=mock_logging_obj,
)
assert cost == 0.5
def test_video_generation_with_files(self):
"""Test video generation with file uploads."""
config = OpenAIVideoConfig()
# Mock file data
mock_file = MagicMock()
mock_file.read.return_value = b"fake_image_data"
data, files, returned_api_base = config.transform_video_create_request(
model="sora-2",
prompt="Test video with image",
api_base="https://api.openai.com/v1/videos",
video_create_optional_request_params={
"input_reference": mock_file,
"seconds": "8",
"size": "720x1280"
},
litellm_params=MagicMock(),
headers={}
)
assert data["model"] == "sora-2"
assert data["prompt"] == "Test video with image"
assert len(files) > 0 # Should have files when input_reference is provided
def test_video_generation_environment_validation(self):
"""Test video generation environment validation."""
config = OpenAIVideoConfig()
# Test environment validation
headers = config.validate_environment(
headers={},
model="sora-2",
api_key="test-api-key"
)
assert "Authorization" in headers
assert headers["Authorization"] == "Bearer test-api-key"
def test_video_generation_uses_api_key_from_litellm_params(self):
"""Test that video generation handler uses api_key from litellm_params when function parameter is None."""
handler = BaseLLMHTTPHandler()
config = OpenAIVideoConfig()
# Mock the validate_environment method to capture the api_key passed to it
with patch.object(config, 'validate_environment') as mock_validate:
mock_validate.return_value = {"Authorization": "Bearer deployment-api-key"}
# Mock the transform and HTTP client
with patch.object(config, 'transform_video_create_request') as mock_transform:
mock_transform.return_value = ({"model": "sora-2", "prompt": "test"}, [], "https://api.openai.com/v1/videos")
# Mock the transform_video_create_response to avoid needing a real response
with patch.object(config, 'transform_video_create_response') as mock_transform_response:
mock_video_object = MagicMock()
mock_video_object.id = "video_123"
mock_video_object.object = "video"
mock_video_object.status = "queued"
mock_transform_response.return_value = mock_video_object
mock_response = MagicMock()
mock_response.json.return_value = {
"id": "video_123",
"object": "video",
"status": "queued",
"created_at": 1712697600,
"model": "sora-2"
}
mock_response.status_code = 200
mock_client = MagicMock()
mock_client.post.return_value = mock_response
with patch(
"litellm.llms.custom_httpx.llm_http_handler._get_httpx_client",
return_value=mock_client,
):
result = handler.video_generation_handler(
model="sora-2",
prompt="test prompt",
video_generation_provider_config=config,
video_generation_optional_request_params={},
custom_llm_provider="openai",
litellm_params={"api_key": "deployment-api-key", "api_base": "https://api.openai.com/v1"},
logging_obj=MagicMock(),
timeout=5.0,
api_key=None, # Function parameter is None
_is_async=False,
)
# Verify validate_environment was called with api_key from litellm_params
mock_validate.assert_called_once()
call_args = mock_validate.call_args
assert call_args.kwargs["api_key"] == "deployment-api-key"
def test_video_generation_url_generation(self):
"""Test video generation URL generation."""
config = OpenAIVideoConfig()
# Test URL generation
url = config.get_complete_url(
model="sora-2",
api_base="https://api.openai.com/v1",
litellm_params={}
)
assert url == "https://api.openai.com/v1/videos"
def test_video_generation_parameter_mapping(self):
"""Test video generation parameter mapping."""
config = OpenAIVideoConfig()
# Test parameter mapping
mapped_params = config.map_openai_params(
video_create_optional_params={
"seconds": "8",
"size": "720x1280",
"user": "test-user"
},
model="sora-2",
drop_params=False
)
assert mapped_params["seconds"] == "8"
assert mapped_params["size"] == "720x1280"
assert mapped_params["user"] == "test-user"
def test_video_generation_unsupported_parameters(self):
"""Test video generation with provider-specific parameters via extra_body."""
from litellm.videos.utils import VideoGenerationRequestUtils
# Test that provider-specific parameters can be passed via extra_body
# This allows support for Vertex AI and Gemini specific parameters
result = VideoGenerationRequestUtils.get_optional_params_video_generation(
model="sora-2",
video_generation_provider_config=OpenAIVideoConfig(),
video_generation_optional_params={
"seconds": "8",
"extra_body": {
"vertex_ai_param": "value",
"gemini_param": "value2"
}
}
)
# extra_body params should be merged into the result
assert result["seconds"] == "8"
assert result["vertex_ai_param"] == "value"
assert result["gemini_param"] == "value2"
# extra_body itself should be removed from the result
assert "extra_body" not in result
def test_video_generation_types(self):
"""Test video generation type definitions."""
# Test VideoObject
video_obj = VideoObject(
id="test_id",
object="video",
status="completed",
created_at=1712697600,
model="sora-2"
)
assert video_obj.id == "test_id"
assert video_obj.object == "video"
assert video_obj.status == "completed"
# Test dictionary-like access
assert video_obj["id"] == "test_id"
assert video_obj["status"] == "completed"
assert "id" in video_obj
assert video_obj.get("id") == "test_id"
assert video_obj.get("nonexistent", "default") == "default"
# Test JSON serialization
json_data = video_obj.json()
assert json_data["id"] == "test_id"
assert json_data["object"] == "video"
def test_video_generation_response_types(self):
"""Test video generation response types."""
# Test VideoResponse
video_obj = VideoObject(
id="test_id",
object="video",
status="completed",
created_at=1712697600
)
response = VideoResponse(data=[video_obj])
assert len(response.data) == 1
assert response.data[0].id == "test_id"
# Test dictionary-like access
assert response["data"][0]["id"] == "test_id"
assert "data" in response
assert response.get("data")[0]["id"] == "test_id"
# Test JSON serialization
json_data = response.json()
assert len(json_data["data"]) == 1
assert json_data["data"][0]["id"] == "test_id"
def test_video_status_basic(self):
"""Test basic video status functionality."""
# Use mock_response parameter for reliable testing
response = video_status(
video_id="video_123",
model="sora-2",
mock_response={
"id": "video_123",
"object": "video",
"status": "completed",
"created_at": 1712697600,
"completed_at": 1712697660,
"model": "sora-2",
"progress": 100,
"size": "720x1280",
"seconds": "8"
}
)
assert isinstance(response, VideoObject)
assert response.id == "video_123"
assert response.status == "completed"
assert response.progress == 100
assert response.model == "sora-2"
def test_video_status_with_mock_response(self):
"""Test video status with mock response."""
mock_data = {
"id": "video_456",
"object": "video",
"status": "processing",
"created_at": 1712697600,
"model": "sora-2",
"progress": 75,
"size": "1280x720",
"seconds": "10"
}
response = video_status(
video_id="video_456",
model="sora-2",
mock_response=mock_data
)
assert isinstance(response, VideoObject)
assert response.id == "video_456"
assert response.status == "processing"
assert response.progress == 75
assert response.model == "sora-2"
def test_video_status_async(self):
"""Test async video status functionality."""
mock_response = VideoObject(
id="video_async_123",
object="video",
status="queued",
created_at=1712697600,
model="sora-2",
progress=0
)
# Mock the async_video_status_handler to return the mock_response
async_mock = AsyncMock(return_value=mock_response)
with patch.object(videos_main.base_llm_http_handler, 'async_video_status_handler', async_mock):
with patch.object(videos_main.base_llm_http_handler, 'video_status_handler', side_effect=lambda **kwargs: async_mock(**kwargs)):
import asyncio
async def test_async():
response = await avideo_status(
video_id="video_async_123",
model="sora-2"
)
return response
response = asyncio.run(test_async())
assert isinstance(response, VideoObject)
assert response.id == "video_async_123"
assert response.status == "queued"
assert response.progress == 0
def test_video_status_parameter_validation(self):
"""Test video status parameter validation."""
# Test with minimal required parameters
response = video_status(
video_id="test_video_id",
model="sora-2",
mock_response={"id": "test", "object": "video", "status": "completed", "created_at": 1712697600}
)
assert isinstance(response, VideoObject)
assert response.id == "test"
def test_video_status_error_handling(self):
"""Test video status error handling."""
with patch.object(videos_main.base_llm_http_handler, 'video_status_handler', side_effect=Exception("API Error")):
with pytest.raises(Exception):
video_status(
video_id="test_video_id",
model="sora-2"
)
def test_video_status_request_transformation(self):
"""Test video status request transformation."""
config = OpenAIVideoConfig()
# Test request transformation
url, data = config.transform_video_status_retrieve_request(
video_id="video_123",
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={}
)
assert url == "https://api.openai.com/v1/videos/video_123"
assert data == {}
def test_video_status_response_transformation(self):
"""Test video status response transformation."""
config = OpenAIVideoConfig()
# Mock HTTP response
mock_http_response = MagicMock()
mock_http_response.json.return_value = {
"id": "video_789",
"object": "video",
"status": "completed",
"created_at": 1712697600,
"completed_at": 1712697660,
"model": "sora-2",
"progress": 100,
"size": "1280x720",
"seconds": "12"
}
response = config.transform_video_status_retrieve_response(
raw_response=mock_http_response,
logging_obj=MagicMock()
)
assert isinstance(response, VideoObject)
assert response.id == "video_789"
assert response.status == "completed"
assert response.progress == 100
assert response.model == "sora-2"
def test_video_status_different_states(self):
"""Test video status with different video states."""
# Test queued state
queued_response = video_status(
video_id="video_queued",
model="sora-2",
mock_response={
"id": "video_queued",
"object": "video",
"status": "queued",
"created_at": 1712697600,
"model": "sora-2",
"progress": 0
}
)
assert queued_response.status == "queued"
assert queued_response.progress == 0
# Test processing state
processing_response = video_status(
video_id="video_processing",
model="sora-2",
mock_response={
"id": "video_processing",
"object": "video",
"status": "processing",
"created_at": 1712697600,
"model": "sora-2",
"progress": 50
}
)
assert processing_response.status == "processing"
assert processing_response.progress == 50
# Test completed state
completed_response = video_status(
video_id="video_completed",
model="sora-2",
mock_response={
"id": "video_completed",
"object": "video",
"status": "completed",
"created_at": 1712697600,
"completed_at": 1712697660,
"model": "sora-2",
"progress": 100
}
)
assert completed_response.status == "completed"
assert completed_response.progress == 100
def test_video_status_with_remix_info(self):
"""Test video status with remix information."""
mock_data = {
"id": "video_remix_123",
"object": "video",
"status": "completed",
"created_at": 1712697600,
"completed_at": 1712697660,
"model": "sora-2",
"progress": 100,
"remixed_from_video_id": "video_original_123",
"size": "720x1280",
"seconds": "8"
}
response = video_status(
video_id="video_remix_123",
model="sora-2",
mock_response=mock_data
)
assert isinstance(response, VideoObject)
assert response.id == "video_remix_123"
assert response.status == "completed"
assert hasattr(response, 'remixed_from_video_id')
assert response.remixed_from_video_id == "video_original_123"
def test_video_status_async_inside_async_function(self):
"""Test that sync video_status works inside async functions (no asyncio.run issues)."""
import asyncio
async def test_sync_in_async():
# This should work without asyncio.run() issues
# Use mock_response parameter for reliable testing
response = video_status(
video_id="video_sync_in_async",
model="sora-2",
mock_response={
"id": "video_sync_in_async",
"object": "video",
"status": "completed",
"created_at": 1712697600,
"model": "sora-2",
"progress": 100
}
)
return response
response = asyncio.run(test_sync_in_async())
assert isinstance(response, VideoObject)
assert response.id == "video_sync_in_async"
assert response.status == "completed"
def test_video_status_url_construction(self):
"""Test video status URL construction."""
config = OpenAIVideoConfig()
# Test with different API bases
test_cases = [
("https://api.openai.com/v1/videos", "video_123", "https://api.openai.com/v1/videos/video_123"),
("https://api.openai.com/v1/videos/", "video_123", "https://api.openai.com/v1/videos/video_123"),
("https://custom-api.com/v1/videos", "video_456", "https://custom-api.com/v1/videos/video_456"),
]
for api_base, video_id, expected_url in test_cases:
url, data = config.transform_video_status_retrieve_request(
video_id=video_id,
api_base=api_base,
litellm_params=MagicMock(),
headers={}
)
assert url == expected_url
assert data == {}
class TestVideoLogging:
"""Test video generation logging functionality."""
class TestVideoLogger(CustomLogger):
def __init__(self):
self.standard_logging_payload = None
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
self.standard_logging_payload = kwargs.get("standard_logging_object")
@pytest.mark.asyncio
async def test_video_generation_logging(self):
"""Test that video generation creates proper logging payload with cost tracking.
Note: Uses AsyncMock with side_effect pattern for reliable parallel execution.
"""
custom_logger = self.TestVideoLogger()
litellm.logging_callback_manager._reset_all_callbacks()
litellm.callbacks = [custom_logger]
# Mock video generation response
mock_response = VideoObject(
id="video_test_123",
object="video",
status="queued",
created_at=1712697600,
model="sora-2",
size="720x1280",
seconds="8"
)
# Create async mock function to return the mock_response
async def mock_async_handler(*args, **kwargs):
return mock_response
# Patch the async_video_generation_handler method on base_llm_http_handler
with patch.object(videos_main.base_llm_http_handler, 'async_video_generation_handler', side_effect=mock_async_handler):
response = await litellm.avideo_generation(
prompt="A cat running in a garden",
model="sora-2",
seconds="8",
size="720x1280"
)
await asyncio.sleep(1) # Allow logging to complete
# Verify logging payload was created
assert custom_logger.standard_logging_payload is not None
payload = custom_logger.standard_logging_payload
# Verify basic logging fields
assert payload["call_type"] == "avideo_generation"
assert payload["status"] == "success"
assert payload["model"] == "sora-2"
assert payload["custom_llm_provider"] == "openai"
# Verify response object is recognized for logging
assert payload["response"] is not None
assert payload["response"]["id"] == "video_test_123"
assert payload["response"]["object"] == "video"
# Verify cost tracking is present (may be 0 in test environment)
assert payload["response_cost"] is not None
# Note: Cost calculation may not work in test environment due to mocking
# The important thing is that the logging payload is created and recognized
def test_openai_transform_video_content_request_empty_params():
"""OpenAI content transform should return empty params to ensure GET is used."""
config = OpenAIVideoConfig()
url, params = config.transform_video_content_request(
video_id="video_123",
api_base="https://api.openai.com/v1/videos",
litellm_params={},
headers={},
)
assert url == "https://api.openai.com/v1/videos/video_123/content"
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
from litellm.types.router import GenericLiteLLMParams
# Clear the HTTP client cache to prevent test isolation issues
# In CI, a cached real HTTPHandler from a previous test might bypass the mock
if hasattr(litellm, 'in_memory_llm_clients_cache'):
litellm.in_memory_llm_clients_cache.flush_cache()
handler = BaseLLMHTTPHandler()
config = OpenAIVideoConfig()
# Use spec=HTTPHandler so isinstance(mock_client, HTTPHandler) returns True,
# ensuring the handler uses our mock directly instead of creating a new client.
mock_client = MagicMock(spec=HTTPHandler)
mock_response = MagicMock()
mock_response.content = b"mp4-bytes"
mock_client.get.return_value = mock_response
# Patch _get_httpx_client to ensure no real HTTP client is created
# This prevents test isolation issues where isinstance check might fail
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
mock_get_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,
)
assert result == b"mp4-bytes"
mock_client.get.assert_called_once()
assert not mock_client.post.called
called_url = mock_client.get.call_args.kwargs["url"]
assert called_url == "https://api.openai.com/v1/videos/video_abc/content"
def test_video_content_respects_api_base_and_api_key_from_kwargs():
"""Test that video_content respects api_base and api_key from kwargs (simulating database entry)."""
from litellm.videos.main import video_content
# Mock the handler to capture litellm_params
captured_litellm_params = None
def capture_litellm_params(*args, **kwargs):
nonlocal captured_litellm_params
captured_litellm_params = kwargs.get("litellm_params")
return b"mp4-bytes"
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
mock_handler.video_content_handler = capture_litellm_params
# Call video_content with api_base and api_key in kwargs (simulating database entry)
# This simulates how the router passes model config from database via **kwargs
result = video_content(
video_id="video_test_123",
custom_llm_provider="azure",
api_base="https://test-resource.openai.azure.com/", # Passed via kwargs by router
api_key="test-api-key-from-db", # Passed via kwargs by router
)
# Verify that api_base and api_key from kwargs were included in litellm_params
assert captured_litellm_params is not None
assert captured_litellm_params.get("api_base") == "https://test-resource.openai.azure.com/"
assert captured_litellm_params.get("api_key") == "test-api-key-from-db"
assert result == b"mp4-bytes"
def test_openai_video_config_has_async_transform():
"""Ensure OpenAIVideoConfig exposes async_transform_video_content_response at runtime."""
cfg = OpenAIVideoConfig()
assert callable(getattr(cfg, "async_transform_video_content_response", None))
def test_gemini_video_config_has_async_transform():
"""Ensure GeminiVideoConfig exposes async_transform_video_content_response at runtime."""
cfg = GeminiVideoConfig()
assert callable(getattr(cfg, "async_transform_video_content_response", None))
def test_encode_video_id_with_provider_handles_azure_video_prefix():
"""
Test that encode_video_id_with_provider correctly encodes Azure/OpenAI video IDs
that start with 'video_' prefix.
This test verifies the fix for the issue where Azure returns video IDs like
'video_69323201cf6081909263f751f89991e6', which were previously skipped
from encoding, causing video status retrieval to default to 'openai' provider.
"""
from litellm.types.videos.utils import (
decode_video_id_with_provider,
encode_video_id_with_provider,
)
# Test case: Azure returns a video ID starting with 'video_'
raw_azure_video_id = "video_69323201cf6081909263f751f89991e6"
provider = "azure"
model_id = "azure/sora-2"
# Encode the video ID with provider information
encoded_id = encode_video_id_with_provider(
video_id=raw_azure_video_id,
provider=provider,
model_id=model_id
)
# Verify the ID was encoded (should be different from the original)
assert encoded_id != raw_azure_video_id
assert encoded_id.startswith("video_")
# Decode the encoded ID to verify provider information is preserved
decoded = decode_video_id_with_provider(encoded_id)
assert decoded.get("custom_llm_provider") == provider
assert decoded.get("model_id") == model_id
assert decoded.get("video_id") == raw_azure_video_id
# Verify that encoding an already-encoded ID doesn't double-encode it
encoded_twice = encode_video_id_with_provider(
video_id=encoded_id,
provider=provider,
model_id=model_id
)
assert encoded_twice == encoded_id # Should return the same encoded ID
class TestVideoListTransformation:
"""Tests for video list request/response transformation with provider ID encoding."""
def test_transform_video_list_response_encodes_first_id_and_last_id(self):
"""Verify that first_id and last_id are encoded with provider metadata."""
config = OpenAIVideoConfig()
mock_http_response = MagicMock()
mock_http_response.json.return_value = {
"object": "list",
"data": [
{
"id": "video_aaa",
"object": "video",
"model": "sora-2",
"status": "completed",
},
{
"id": "video_bbb",
"object": "video",
"model": "sora-2",
"status": "completed",
},
],
"first_id": "video_aaa",
"last_id": "video_bbb",
"has_more": False,
}
result = config.transform_video_list_response(
raw_response=mock_http_response,
logging_obj=MagicMock(),
custom_llm_provider="azure",
)
from litellm.types.videos.utils import decode_video_id_with_provider
# data[].id should be encoded
for item in result["data"]:
decoded = decode_video_id_with_provider(item["id"])
assert decoded["custom_llm_provider"] == "azure"
# first_id and last_id should also be encoded
first_decoded = decode_video_id_with_provider(result["first_id"])
assert first_decoded["custom_llm_provider"] == "azure"
assert first_decoded["video_id"] == "video_aaa"
assert first_decoded["model_id"] == "sora-2"
last_decoded = decode_video_id_with_provider(result["last_id"])
assert last_decoded["custom_llm_provider"] == "azure"
assert last_decoded["video_id"] == "video_bbb"
assert last_decoded["model_id"] == "sora-2"
def test_transform_video_list_response_no_provider_leaves_ids_unchanged(self):
"""When custom_llm_provider is None, all IDs should remain unchanged."""
config = OpenAIVideoConfig()
mock_http_response = MagicMock()
mock_http_response.json.return_value = {
"object": "list",
"data": [
{"id": "video_aaa", "object": "video", "model": "sora-2", "status": "completed"},
],
"first_id": "video_aaa",
"last_id": "video_aaa",
"has_more": False,
}
result = config.transform_video_list_response(
raw_response=mock_http_response,
logging_obj=MagicMock(),
custom_llm_provider=None,
)
assert result["data"][0]["id"] == "video_aaa"
assert result["first_id"] == "video_aaa"
assert result["last_id"] == "video_aaa"
def test_transform_video_list_response_missing_pagination_fields(self):
"""first_id / last_id may be absent or null; should not raise."""
config = OpenAIVideoConfig()
mock_http_response = MagicMock()
mock_http_response.json.return_value = {
"object": "list",
"data": [
{"id": "video_aaa", "object": "video", "model": "sora-2", "status": "completed"},
],
"has_more": False,
}
result = config.transform_video_list_response(
raw_response=mock_http_response,
logging_obj=MagicMock(),
custom_llm_provider="azure",
)
# data[].id should still be encoded
from litellm.types.videos.utils import decode_video_id_with_provider
decoded = decode_video_id_with_provider(result["data"][0]["id"])
assert decoded["custom_llm_provider"] == "azure"
# first_id / last_id should not be present
assert "first_id" not in result
assert "last_id" not in result
def test_transform_video_list_request_decodes_after_parameter(self):
"""Encoded 'after' cursor should be decoded back to the raw provider ID."""
from litellm.types.videos.utils import encode_video_id_with_provider
config = OpenAIVideoConfig()
raw_id = "video_69888baee890819086dd3366bfc372fe"
encoded_id = encode_video_id_with_provider(raw_id, "azure", "sora-2")
url, params = config.transform_video_list_request(
api_base="https://my-resource.openai.azure.com/openai/v1/videos",
litellm_params=MagicMock(),
headers={},
after=encoded_id,
limit=10,
)
assert params["after"] == raw_id
assert params["limit"] == "10"
def test_transform_video_list_request_passes_through_plain_after(self):
"""A plain (non-encoded) 'after' value should pass through unchanged."""
config = OpenAIVideoConfig()
url, params = config.transform_video_list_request(
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
after="video_plain_id",
)
assert params["after"] == "video_plain_id"
def test_transform_video_list_roundtrip(self):
"""first_id from list response should decode correctly when used as after parameter."""
config = OpenAIVideoConfig()
# Simulate a list response
mock_http_response = MagicMock()
mock_http_response.json.return_value = {
"object": "list",
"data": [
{"id": "video_aaa", "object": "video", "model": "sora-2", "status": "completed"},
{"id": "video_bbb", "object": "video", "model": "sora-2", "status": "completed"},
],
"first_id": "video_aaa",
"last_id": "video_bbb",
"has_more": True,
}
list_result = config.transform_video_list_response(
raw_response=mock_http_response,
logging_obj=MagicMock(),
custom_llm_provider="azure",
)
# Use the encoded last_id as the 'after' cursor for the next page
_, params = config.transform_video_list_request(
api_base="https://my-resource.openai.azure.com/openai/v1/videos",
litellm_params=MagicMock(),
headers={},
after=list_result["last_id"],
)
# The after param sent to the upstream API should be the raw video ID
assert params["after"] == "video_bbb"
class TestVideoEndpointsProxyLitellmParams:
"""Test that video proxy endpoints (status, content, remix) respect litellm_params from proxy config."""
@pytest.fixture
def client_with_vertex_config(self, monkeypatch):
"""Create a test client with a proxy config that includes Vertex AI model with litellm_params."""
import asyncio
import tempfile
import yaml
from fastapi import FastAPI
from fastapi.testclient import TestClient
from litellm.proxy.proxy_server import (
cleanup_router_config_variables,
initialize,
router,
)
from litellm.proxy.video_endpoints.endpoints import router as video_router
# Clean up any existing router config
cleanup_router_config_variables()
# Create inline config
config = {
"model_list": [
{
"model_name": "vertex-ai-sora-2",
"litellm_params": {
"model": "vertex_ai/veo-2.0-generate-001",
"vertex_project": "test-project-123",
"vertex_location": "global",
"vertex_credentials": "/path/to/test-credentials.json",
}
}
]
}
# Write config to temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(config, f)
config_fp = f.name
try:
# Initialize the proxy with the test config
app = FastAPI()
asyncio.run(initialize(config=config_fp, debug=True))
app.include_router(router)
app.include_router(video_router)
return TestClient(app)
finally:
# Clean up temporary file
import os
if os.path.exists(config_fp):
os.unlink(config_fp)
@pytest.fixture
def mock_video_generation_response(self):
"""Mock video generation response with encoded video_id."""
from litellm.types.videos.utils import encode_video_id_with_provider
# Create an encoded video_id that includes provider and model_id
original_video_id = "projects/test-project-123/locations/global/publishers/google/models/veo-2.0-generate-001/operations/test-operation-123"
encoded_video_id = encode_video_id_with_provider(
video_id=original_video_id,
provider="vertex_ai",
model_id="veo-2.0-generate-001",
)
return VideoObject(
id=encoded_video_id,
object="video",
status="processing",
created_at=1712697600,
model="vertex_ai/veo-2.0-generate-001",
)
@pytest.fixture
def mock_video_status_response(self):
"""Mock video status response."""
return VideoObject(
id="video_test_123",
object="video",
status="completed",
created_at=1712697600,
completed_at=1712697660,
model="vertex_ai/veo-2.0-generate-001",
progress=100,
)
@pytest.fixture
def mock_video_content_response(self):
"""Mock video content response (raw bytes)."""
return b"fake_video_content_bytes"
@pytest.mark.asyncio
async def test_video_status_respects_litellm_params(
self, client_with_vertex_config, mock_video_generation_response, mock_video_status_response
):
"""Test that video_status endpoint uses litellm_params from proxy config."""
from unittest.mock import AsyncMock, MagicMock, patch
# Create an encoded video_id
encoded_video_id = mock_video_generation_response.id
# Mock the router instance
mock_router_instance = MagicMock()
mock_router_instance.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2"
mock_router_instance.model_names = {"vertex-ai-sora-2"}
mock_router_instance.has_model_id.return_value = False
# Mock route_request to capture the data being passed
# route_request should return a coroutine (not await it), so we return a coroutine
async def mock_route_request_func(*args, **kwargs):
return mock_video_status_response
# Create a coroutine that will be added to tasks
def create_mock_coroutine(*args, **kwargs):
return mock_route_request_func(*args, **kwargs)
with patch("litellm.proxy.proxy_server.llm_router", mock_router_instance):
with patch("litellm.proxy.common_request_processing.route_request", side_effect=create_mock_coroutine) as mock_route_request:
# Make request to video_status endpoint
response = client_with_vertex_config.get(
f"/v1/videos/{encoded_video_id}",
headers={"Authorization": "Bearer sk-1234"},
)
# Verify the endpoint was called
assert response.status_code == 200, f"Response: {response.text}"
# Verify that route_request was called
assert mock_route_request.called
call_args = mock_route_request.call_args
# route_request is called with data as a keyword argument
data_passed = call_args.kwargs.get("data", {}) if call_args.kwargs else (call_args.args[0] if call_args.args and len(call_args.args) > 0 else {})
# Verify that model was resolved and added to data
assert data_passed.get("model") == "vertex-ai-sora-2", (
f"Expected model to be 'vertex-ai-sora-2', got '{data_passed.get('model')}'. "
f"Full data: {data_passed}, call_args: {call_args}"
)
# Verify that custom_llm_provider is set from decoded video_id
assert data_passed.get("custom_llm_provider") == "vertex_ai", (
f"Expected custom_llm_provider to be 'vertex_ai', got '{data_passed.get('custom_llm_provider')}'. "
f"Full data: {data_passed}"
)
@pytest.mark.asyncio
async def test_video_content_respects_litellm_params(
self, client_with_vertex_config, mock_video_generation_response, mock_video_content_response
):
"""Test that video_content endpoint uses litellm_params from proxy config."""
from unittest.mock import AsyncMock, MagicMock, patch
# Create an encoded video_id
encoded_video_id = mock_video_generation_response.id
# Mock the router instance
mock_router_instance = MagicMock()
mock_router_instance.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2"
mock_router_instance.model_names = {"vertex-ai-sora-2"}
mock_router_instance.has_model_id.return_value = False
# Mock route_request to capture the data being passed
# route_request should return a coroutine (not await it), so we return a coroutine
async def mock_route_request_func(*args, **kwargs):
return mock_video_content_response
# Create a coroutine that will be added to tasks
def create_mock_coroutine(*args, **kwargs):
return mock_route_request_func(*args, **kwargs)
with patch("litellm.proxy.proxy_server.llm_router", mock_router_instance):
with patch("litellm.proxy.common_request_processing.route_request", side_effect=create_mock_coroutine) as mock_route_request:
# Make request to video_content endpoint
response = client_with_vertex_config.get(
f"/v1/videos/{encoded_video_id}/content",
headers={"Authorization": "Bearer sk-1234"},
)
# Verify the endpoint was called
assert response.status_code == 200, f"Response: {response.text}"
# Verify that route_request was called
assert mock_route_request.called
call_args = mock_route_request.call_args
# route_request is called with data as a keyword argument
data_passed = call_args.kwargs.get("data", {}) if call_args.kwargs else (call_args.args[0] if call_args.args and len(call_args.args) > 0 else {})
# Verify that model was resolved and added to data
assert data_passed.get("model") == "vertex-ai-sora-2", (
f"Expected model to be 'vertex-ai-sora-2', got '{data_passed.get('model')}'. "
f"Full data: {data_passed}, call_args: {call_args}"
)
# Verify that custom_llm_provider is correctly set from decoded video_id (not "openai")
assert data_passed.get("custom_llm_provider") == "vertex_ai", (
f"Expected custom_llm_provider to be 'vertex_ai', got '{data_passed.get('custom_llm_provider')}'. "
f"Full data: {data_passed}"
)
@pytest.mark.asyncio
async def test_video_content_preserves_custom_llm_provider_from_decoded_id(
self, client_with_vertex_config, mock_video_generation_response, mock_video_content_response
):
"""Test that video_content preserves custom_llm_provider from decoded video_id."""
from unittest.mock import AsyncMock, MagicMock, patch
# Create an encoded video_id
encoded_video_id = mock_video_generation_response.id
# Mock the router instance
mock_router_instance = MagicMock()
mock_router_instance.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2"
mock_router_instance.model_names = {"vertex-ai-sora-2"}
mock_router_instance.has_model_id.return_value = False
# Mock route_request to capture the data being passed
# route_request should return a coroutine (not await it), so we return a coroutine
async def mock_route_request_func(*args, **kwargs):
return mock_video_content_response
# Create a coroutine that will be added to tasks
def create_mock_coroutine(*args, **kwargs):
return mock_route_request_func(*args, **kwargs)
with patch("litellm.proxy.proxy_server.llm_router", mock_router_instance):
with patch("litellm.proxy.common_request_processing.route_request", side_effect=create_mock_coroutine) as mock_route_request:
# Make request to video_content endpoint
response = client_with_vertex_config.get(
f"/v1/videos/{encoded_video_id}/content",
headers={"Authorization": "Bearer sk-1234"},
)
# Verify the endpoint was called
assert response.status_code == 200, f"Response: {response.text}"
# Verify that route_request was called
assert mock_route_request.called
call_args = mock_route_request.call_args
# route_request is called with data as a keyword argument
data_passed = call_args.kwargs.get("data", {}) if call_args.kwargs else (call_args.args[0] if call_args.args and len(call_args.args) > 0 else {})
# Most importantly: verify that custom_llm_provider is "vertex_ai" not "openai"
# This was the bug we fixed - it was defaulting to "openai" before
assert data_passed.get("custom_llm_provider") == "vertex_ai", (
f"Expected custom_llm_provider to be 'vertex_ai', "
f"but got '{data_passed.get('custom_llm_provider')}'. "
f"Full data: {data_passed}, call_args: {call_args}"
)
def test_video_remix_handler_uses_api_key_from_litellm_params():
"""Sync remix handler should fall back to litellm_params api_key when api_key param is None."""
handler = BaseLLMHTTPHandler()
config = OpenAIVideoConfig()
with patch.object(config, "validate_environment") as mock_validate:
mock_validate.return_value = {"Authorization": "Bearer deployment-key"}
with patch.object(config, "transform_video_remix_request") as mock_transform:
mock_transform.return_value = ("https://api.openai.com/v1/videos/video_123/remix", {"prompt": "remix it"})
with patch.object(config, "transform_video_remix_response") as mock_resp:
mock_resp.return_value = MagicMock()
mock_client = MagicMock()
mock_client.post.return_value = MagicMock(status_code=200)
with patch(
"litellm.llms.custom_httpx.llm_http_handler._get_httpx_client",
return_value=mock_client,
):
handler.video_remix_handler(
video_id="video_123",
prompt="remix it",
video_remix_provider_config=config,
custom_llm_provider="openai",
litellm_params={"api_key": "deployment-key", "api_base": "https://api.openai.com/v1"},
logging_obj=MagicMock(),
timeout=5.0,
api_key=None,
_is_async=False,
)
mock_validate.assert_called_once()
assert mock_validate.call_args.kwargs["api_key"] == "deployment-key"
@pytest.mark.asyncio
async def test_async_video_remix_handler_uses_api_key_from_litellm_params():
"""Async remix handler should fall back to litellm_params api_key when api_key param is None."""
handler = BaseLLMHTTPHandler()
config = OpenAIVideoConfig()
with patch.object(config, "validate_environment") as mock_validate:
mock_validate.return_value = {"Authorization": "Bearer deployment-key"}
with patch.object(config, "transform_video_remix_request") as mock_transform:
mock_transform.return_value = ("https://api.openai.com/v1/videos/video_123/remix", {"prompt": "remix it"})
with patch.object(config, "transform_video_remix_response") as mock_resp:
mock_resp.return_value = MagicMock()
mock_client = MagicMock(spec=AsyncHTTPHandler)
mock_response = MagicMock(status_code=200)
mock_client.post = AsyncMock(return_value=mock_response)
with patch(
"litellm.llms.custom_httpx.llm_http_handler.get_async_httpx_client",
return_value=mock_client,
):
await handler.async_video_remix_handler(
video_id="video_123",
prompt="remix it",
video_remix_provider_config=config,
custom_llm_provider="openai",
litellm_params={"api_key": "deployment-key", "api_base": "https://api.openai.com/v1"},
logging_obj=MagicMock(),
timeout=5.0,
api_key=None,
)
mock_validate.assert_called_once()
assert mock_validate.call_args.kwargs["api_key"] == "deployment-key"
def test_video_remix_handler_prefers_explicit_api_key():
"""Sync remix handler should prefer explicit api_key over litellm_params."""
handler = BaseLLMHTTPHandler()
config = OpenAIVideoConfig()
with patch.object(config, "validate_environment") as mock_validate:
mock_validate.return_value = {"Authorization": "Bearer explicit-key"}
with patch.object(config, "transform_video_remix_request") as mock_transform:
mock_transform.return_value = ("https://api.openai.com/v1/videos/video_123/remix", {"prompt": "remix it"})
with patch.object(config, "transform_video_remix_response") as mock_resp:
mock_resp.return_value = MagicMock()
mock_client = MagicMock()
mock_client.post.return_value = MagicMock(status_code=200)
with patch(
"litellm.llms.custom_httpx.llm_http_handler._get_httpx_client",
return_value=mock_client,
):
handler.video_remix_handler(
video_id="video_123",
prompt="remix it",
video_remix_provider_config=config,
custom_llm_provider="openai",
litellm_params={"api_key": "deployment-key", "api_base": "https://api.openai.com/v1"},
logging_obj=MagicMock(),
timeout=5.0,
api_key="explicit-key",
_is_async=False,
)
mock_validate.assert_called_once()
assert mock_validate.call_args.kwargs["api_key"] == "explicit-key"
if __name__ == "__main__":
pytest.main([__file__])
# ===== Tests for new video endpoints (characters, edits, extensions) =====
class TestVideoCreateCharacter:
"""Tests for video_create_character / avideo_create_character."""
def test_video_create_character_transform_request(self):
"""Verify multipart form construction for POST /videos/characters."""
config = OpenAIVideoConfig()
fake_video = b"fake_video_bytes"
url, files_list = config.transform_video_create_character_request(
name="hero",
video=fake_video,
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
)
assert url == "https://api.openai.com/v1/videos/characters"
# Should have (name field) + (video file field) = 2 entries
assert len(files_list) == 2
field_names = [f[0] for f in files_list]
assert "name" in field_names
assert "video" in field_names
def test_video_create_character_sets_video_mimetype(self):
"""Ensure character video upload is sent as video/mp4."""
config = OpenAIVideoConfig()
fake_video = io.BytesIO(b"....ftyp....video-bytes")
fake_video.name = "character.mp4"
_, files_list = config.transform_video_create_character_request(
name="hero",
video=fake_video,
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
)
video_parts = [f for f in files_list if f[0] == "video"]
assert len(video_parts) == 1
video_tuple = video_parts[0][1]
assert video_tuple[0] == "character.mp4"
assert video_tuple[2] == "video/mp4"
def test_video_create_character_transform_response(self):
"""Verify CharacterObject is returned from response."""
from litellm.types.videos.main import CharacterObject
config = OpenAIVideoConfig()
mock_response = MagicMock()
mock_response.json.return_value = {
"id": "char_abc123",
"object": "character",
"created_at": 1712697600,
"name": "hero",
}
result = config.transform_video_create_character_response(
raw_response=mock_response,
logging_obj=MagicMock(),
)
assert isinstance(result, CharacterObject)
assert result.id == "char_abc123"
assert result.name == "hero"
def test_video_create_character_mock_response(self):
"""video_create_character returns CharacterObject on mock_response."""
from litellm.types.videos.main import CharacterObject
from litellm.videos.main import video_create_character
response = video_create_character(
name="hero",
video=b"fake",
mock_response={
"id": "char_abc",
"object": "character",
"created_at": 1712697600,
"name": "hero",
},
)
assert isinstance(response, CharacterObject)
assert response.id == "char_abc"
class TestVideoGetCharacter:
"""Tests for video_get_character / avideo_get_character."""
def test_video_get_character_transform_request(self):
"""Verify URL construction for GET /videos/characters/{character_id}."""
config = OpenAIVideoConfig()
url, params = config.transform_video_get_character_request(
character_id="char_xyz",
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
)
assert url == "https://api.openai.com/v1/videos/characters/char_xyz"
assert params == {}
def test_video_get_character_transform_response(self):
"""Verify CharacterObject is returned from GET response."""
from litellm.types.videos.main import CharacterObject
config = OpenAIVideoConfig()
mock_response = MagicMock()
mock_response.json.return_value = {
"id": "char_xyz",
"object": "character",
"created_at": 1712697600,
"name": "villain",
}
result = config.transform_video_get_character_response(
raw_response=mock_response,
logging_obj=MagicMock(),
)
assert isinstance(result, CharacterObject)
assert result.id == "char_xyz"
assert result.name == "villain"
def test_video_get_character_mock_response(self):
"""video_get_character returns CharacterObject on mock_response."""
from litellm.types.videos.main import CharacterObject
from litellm.videos.main import video_get_character
response = video_get_character(
character_id="char_xyz",
mock_response={
"id": "char_xyz",
"object": "character",
"created_at": 1712697600,
"name": "villain",
},
)
assert isinstance(response, CharacterObject)
assert response.id == "char_xyz"
class TestVideoEdit:
"""Tests for video_edit / avideo_edit."""
def test_video_edit_transform_request(self):
"""Verify JSON body with video.id for POST /videos/edits."""
config = OpenAIVideoConfig()
url, data = config.transform_video_edit_request(
prompt="make it brighter",
video_id="video_abc123",
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
)
assert url == "https://api.openai.com/v1/videos/edits"
assert data["prompt"] == "make it brighter"
assert data["video"]["id"] == "video_abc123"
def test_video_edit_transform_request_with_extra_body(self):
"""Extra body params are merged into request data."""
config = OpenAIVideoConfig()
url, data = config.transform_video_edit_request(
prompt="darken it",
video_id="video_abc123",
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
extra_body={"resolution": "1080p"},
)
assert data["resolution"] == "1080p"
def test_video_edit_mock_response(self):
"""video_edit returns VideoObject on mock_response."""
from litellm.videos.main import video_edit
response = video_edit(
video_id="video_abc123",
prompt="make it brighter",
mock_response={
"id": "video_edit_001",
"object": "video",
"status": "queued",
"created_at": 1712697600,
},
)
assert isinstance(response, VideoObject)
assert response.id == "video_edit_001"
def test_video_edit_strips_encoded_provider_from_video_id(self):
"""Provider-encoded video IDs are decoded before sending to API."""
from litellm.types.videos.utils import encode_video_id_with_provider
config = OpenAIVideoConfig()
encoded_id = encode_video_id_with_provider("raw_video_id", "openai", None)
url, data = config.transform_video_edit_request(
prompt="test",
video_id=encoded_id,
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
)
# The video.id in the request body should be the raw ID, not the encoded one
assert data["video"]["id"] == "raw_video_id"
class TestVideoExtension:
"""Tests for video_extension / avideo_extension."""
def test_video_extension_transform_request(self):
"""Verify JSON body with video.id + seconds for POST /videos/extensions."""
config = OpenAIVideoConfig()
url, data = config.transform_video_extension_request(
prompt="continue the scene",
video_id="video_abc123",
seconds="5",
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
)
assert url == "https://api.openai.com/v1/videos/extensions"
assert data["prompt"] == "continue the scene"
assert data["seconds"] == "5"
assert data["video"]["id"] == "video_abc123"
def test_video_extension_transform_request_with_extra_body(self):
"""Extra body params are merged into request data."""
config = OpenAIVideoConfig()
url, data = config.transform_video_extension_request(
prompt="extend",
video_id="video_abc123",
seconds="10",
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
extra_body={"model": "sora-2"},
)
assert data["model"] == "sora-2"
def test_video_extension_mock_response(self):
"""video_extension returns VideoObject on mock_response."""
from litellm.videos.main import video_extension
response = video_extension(
video_id="video_abc123",
prompt="continue the scene",
seconds="5",
mock_response={
"id": "video_ext_001",
"object": "video",
"status": "queued",
"created_at": 1712697600,
},
)
assert isinstance(response, VideoObject)
assert response.id == "video_ext_001"
def test_video_extension_strips_encoded_provider_from_video_id(self):
"""Provider-encoded video IDs are decoded before sending to API."""
from litellm.types.videos.utils import encode_video_id_with_provider
config = OpenAIVideoConfig()
encoded_id = encode_video_id_with_provider("raw_video_id", "openai", None)
url, data = config.transform_video_extension_request(
prompt="extend",
video_id=encoded_id,
seconds="5",
api_base="https://api.openai.com/v1/videos",
litellm_params=MagicMock(),
headers={},
)
assert data["video"]["id"] == "raw_video_id"
@pytest.fixture
def video_proxy_test_client():
from fastapi import FastAPI
from fastapi.testclient import TestClient
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.video_endpoints.endpoints import router as video_router
app = FastAPI()
app.include_router(video_router)
app.dependency_overrides[user_api_key_auth] = lambda: MagicMock()
return TestClient(app)
def test_character_id_encode_decode_roundtrip():
from litellm.types.videos.utils import (
decode_character_id_with_provider,
encode_character_id_with_provider,
)
encoded = encode_character_id_with_provider(
character_id="char_raw_123",
provider="vertex_ai",
model_id="veo-2.0-generate-001",
)
decoded = decode_character_id_with_provider(encoded)
assert decoded["character_id"] == "char_raw_123"
assert decoded["custom_llm_provider"] == "vertex_ai"
assert decoded["model_id"] == "veo-2.0-generate-001"
def test_character_id_decode_handles_missing_base64_padding():
from litellm.types.videos.utils import (
decode_character_id_with_provider,
encode_character_id_with_provider,
)
encoded = encode_character_id_with_provider(
character_id="id",
provider="openai",
model_id="gpt-4o",
)
encoded_without_padding = encoded.rstrip("=")
decoded = decode_character_id_with_provider(encoded_without_padding)
assert decoded["character_id"] == "id"
assert decoded["custom_llm_provider"] == "openai"
assert decoded["model_id"] == "gpt-4o"
def test_video_create_character_target_model_names_returns_encoded_id(video_proxy_test_client):
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
from litellm.types.videos.utils import decode_character_id_with_provider
captured_data = {}
async def _mock_base_process(self, **kwargs):
captured_data.update(self.data)
return {
"id": "char_upstream_123",
"object": "character",
"created_at": 1712697600,
"name": "hero",
}
with patch.object(
ProxyBaseLLMRequestProcessing,
"base_process_llm_request",
new=_mock_base_process,
):
response = video_proxy_test_client.post(
"/v1/videos/characters",
headers={"Authorization": "Bearer sk-1234"},
files={"video": ("character.mp4", b"fake-video", "video/mp4")},
data={
"name": "hero",
"target_model_names": "vertex-ai-sora-2",
"extra_body": json.dumps({"custom_llm_provider": "vertex_ai"}),
},
)
assert response.status_code == 200, response.text
response_json = response.json()
decoded = decode_character_id_with_provider(response_json["id"])
assert decoded["character_id"] == "char_upstream_123"
assert decoded["custom_llm_provider"] == "vertex_ai"
assert decoded["model_id"] == "vertex-ai-sora-2"
assert captured_data["model"] == "vertex-ai-sora-2"
assert captured_data["custom_llm_provider"] == "vertex_ai"
def test_video_get_character_accepts_encoded_character_id(video_proxy_test_client):
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
from litellm.types.videos.utils import (
decode_character_id_with_provider,
encode_character_id_with_provider,
)
captured_data = {}
async def _mock_base_process(self, **kwargs):
captured_data.update(self.data)
return {
"id": "char_upstream_123",
"object": "character",
"created_at": 1712697600,
"name": "hero",
}
encoded_character_id = encode_character_id_with_provider(
character_id="char_upstream_123",
provider="vertex_ai",
model_id="veo-2.0-generate-001",
)
mock_router = MagicMock()
mock_router.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2"
with patch("litellm.proxy.proxy_server.llm_router", mock_router):
with patch.object(
ProxyBaseLLMRequestProcessing,
"base_process_llm_request",
new=_mock_base_process,
):
response = video_proxy_test_client.get(
f"/v1/videos/characters/{encoded_character_id}",
headers={"Authorization": "Bearer sk-1234"},
)
assert response.status_code == 200, response.text
assert captured_data["character_id"] == "char_upstream_123"
assert captured_data["custom_llm_provider"] == "vertex_ai"
assert captured_data["model"] == "vertex-ai-sora-2"
response_decoded = decode_character_id_with_provider(response.json()["id"])
assert response_decoded["character_id"] == "char_upstream_123"
assert response_decoded["custom_llm_provider"] == "vertex_ai"
assert response_decoded["model_id"] == "veo-2.0-generate-001"
@pytest.mark.parametrize("endpoint", ["/v1/videos/edits", "/v1/videos/extensions"])
def test_edit_and_extension_support_custom_provider_from_extra_body(
video_proxy_test_client, endpoint
):
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
captured_data = {}
async def _mock_base_process(self, **kwargs):
captured_data.update(self.data)
return {
"id": "video_resp_123",
"object": "video",
"status": "queued",
"created_at": 1712697600,
}
payload = {
"prompt": "test",
"video": {"id": "video_raw_123"},
"extra_body": {"custom_llm_provider": "vertex_ai"},
}
if endpoint.endswith("extensions"):
payload["seconds"] = "4"
with patch.object(
ProxyBaseLLMRequestProcessing,
"base_process_llm_request",
new=_mock_base_process,
):
response = video_proxy_test_client.post(
endpoint,
headers={"Authorization": "Bearer sk-1234"},
json=payload,
)
assert response.status_code == 200, response.text
assert captured_data["custom_llm_provider"] == "vertex_ai"
@pytest.mark.parametrize("endpoint", ["/v1/videos/edits", "/v1/videos/extensions"])
def test_edit_and_extension_route_with_encoded_video_ids(
video_proxy_test_client, endpoint
):
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
from litellm.types.videos.utils import encode_video_id_with_provider
captured_data = {}
async def _mock_base_process(self, **kwargs):
captured_data.update(self.data)
return {
"id": "video_resp_123",
"object": "video",
"status": "queued",
"created_at": 1712697600,
}
encoded_video_id = encode_video_id_with_provider(
video_id="video_raw_123",
provider="vertex_ai",
model_id="veo-2.0-generate-001",
)
payload = {"prompt": "test", "video": {"id": encoded_video_id}}
if endpoint.endswith("extensions"):
payload["seconds"] = "4"
mock_router = MagicMock()
mock_router.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2"
with patch("litellm.proxy.proxy_server.llm_router", mock_router):
with patch.object(
ProxyBaseLLMRequestProcessing,
"base_process_llm_request",
new=_mock_base_process,
):
response = video_proxy_test_client.post(
endpoint,
headers={"Authorization": "Bearer sk-1234"},
json=payload,
)
assert response.status_code == 200, response.text
assert captured_data["video_id"] == encoded_video_id
assert captured_data["custom_llm_provider"] == "vertex_ai"
assert captured_data["model"] == "vertex-ai-sora-2"