From 245372c4a6b3da9e4db6d3b69bba9169c8287640 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 14 Oct 2025 12:32:47 +0530 Subject: [PATCH 01/17] Add openai videos generation and retrieval support --- litellm/__init__.py | 4 + litellm/constants.py | 1 + .../get_llm_provider_logic.py | 1 + .../video_retrieval/transformation.py | 90 ++ .../videos_generation/transformation.py | 119 +++ litellm/llms/custom_httpx/llm_http_handler.py | 367 +++++++++ .../openai/video_generation/transformation.py | 182 +++++ .../openai/video_retrieval/transformation.py | 99 +++ ...odel_prices_and_context_window_backup.json | 7 + litellm/types/llms/openai.py | 85 ++ litellm/types/utils.py | 14 + litellm/types/videos/main.py | 112 +++ litellm/utils.py | 14 + litellm/videos/main.py | 767 ++++++++++++++++++ litellm/videos/utils.py | 71 ++ 15 files changed, 1933 insertions(+) create mode 100644 litellm/llms/base_llm/video_retrieval/transformation.py create mode 100644 litellm/llms/base_llm/videos_generation/transformation.py create mode 100644 litellm/llms/openai/video_generation/transformation.py create mode 100644 litellm/llms/openai/video_retrieval/transformation.py create mode 100644 litellm/types/videos/main.py create mode 100644 litellm/videos/main.py create mode 100644 litellm/videos/utils.py diff --git a/litellm/__init__.py b/litellm/__init__.py index a871279d37ff..0965f5921b7c 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -980,6 +980,9 @@ def add_known_models(): ####### IMAGE GENERATION MODELS ################### openai_image_generation_models = ["dall-e-2", "dall-e-3"] +####### VIDEO GENERATION MODELS ################### +openai_video_generation_models = ["sora-2"] + from .timeout import timeout from .cost_calculator import completion_cost from litellm.litellm_core_utils.litellm_logging import Logging, modify_integration @@ -1329,6 +1332,7 @@ def add_known_models(): from .assistants.main import * from .batches.main import * from .images.main import * +from .videos.main import * from .batch_completion.main import * # type: ignore from .rerank_api.main import * from .llms.anthropic.experimental_pass_through.messages.handler import * diff --git a/litellm/constants.py b/litellm/constants.py index 64e92e382f86..50ad1e89ad64 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -265,6 +265,7 @@ "high": 10, } DEFAULT_IMAGE_ENDPOINT_MODEL = "dall-e-2" +DEFAULT_VIDEO_ENDPOINT_MODEL = "sora-2" LITELLM_CHAT_PROVIDERS = [ "openai", diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 275f2a63a1ba..fb25c5ed8403 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -279,6 +279,7 @@ def get_llm_provider( # noqa: PLR0915 or "ft:gpt-3.5-turbo" in model or "ft:gpt-4" in model # catches ft:gpt-4-0613, ft:gpt-4o or model in litellm.openai_image_generation_models + or model in litellm.openai_video_generation_models ): custom_llm_provider = "openai" elif model in litellm.open_ai_text_completion_models: diff --git a/litellm/llms/base_llm/video_retrieval/transformation.py b/litellm/llms/base_llm/video_retrieval/transformation.py new file mode 100644 index 000000000000..db48d821444f --- /dev/null +++ b/litellm/llms/base_llm/video_retrieval/transformation.py @@ -0,0 +1,90 @@ +import types +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Dict, Optional, Union + +import httpx + +from litellm.types.router import GenericLiteLLMParams + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from litellm.types.videos.main import VideoResponse as _VideoResponse + + from ..chat.transformation import BaseLLMException as _BaseLLMException + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseLLMException = _BaseLLMException + VideoResponse = _VideoResponse +else: + LiteLLMLoggingObj = Any + BaseLLMException = Any + VideoResponse = Any + + +class BaseVideoRetrievalConfig(ABC): + def __init__(self): + pass + + @classmethod + def get_config(cls): + return { + k: v + for k, v in cls.__dict__.items() + if not k.startswith("__") + and not k.startswith("_abc") + and not isinstance( + v, + ( + types.FunctionType, + types.BuiltinFunctionType, + classmethod, + staticmethod, + ), + ) + and v is not None + } + + @abstractmethod + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + return {} + + @abstractmethod + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete url for the request + + Some providers need `model` in `api_base` + """ + if api_base is None: + raise ValueError("api_base is required") + return api_base + + @abstractmethod + def transform_video_retrieve_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoResponse: + pass + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from ..chat.transformation import BaseLLMException + + raise BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) \ No newline at end of file diff --git a/litellm/llms/base_llm/videos_generation/transformation.py b/litellm/llms/base_llm/videos_generation/transformation.py new file mode 100644 index 000000000000..61cc03bbf04d --- /dev/null +++ b/litellm/llms/base_llm/videos_generation/transformation.py @@ -0,0 +1,119 @@ +import types +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union + +import httpx +from httpx._types import RequestFiles + +from litellm.types.videos.main import VideoCreateOptionalRequestParams +from litellm.types.responses.main import * +from litellm.types.router import GenericLiteLLMParams + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from litellm.types.videos.main import VideoResponse as _VideoResponse + + from ..chat.transformation import BaseLLMException as _BaseLLMException + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseLLMException = _BaseLLMException + VideoResponse = _VideoResponse +else: + LiteLLMLoggingObj = Any + BaseLLMException = Any + VideoResponse = Any + + +class BaseVideoGenerationConfig(ABC): + def __init__(self): + pass + + @classmethod + def get_config(cls): + return { + k: v + for k, v in cls.__dict__.items() + if not k.startswith("__") + and not k.startswith("_abc") + and not isinstance( + v, + ( + types.FunctionType, + types.BuiltinFunctionType, + classmethod, + staticmethod, + ), + ) + and v is not None + } + + @abstractmethod + def get_supported_openai_params(self, model: str) -> list: + pass + + @abstractmethod + def map_openai_params( + self, + video_create_optional_params: VideoCreateOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + pass + + @abstractmethod + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + return {} + + @abstractmethod + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + OPTIONAL + + Get the complete url for the request + + Some providers need `model` in `api_base` + """ + if api_base is None: + raise ValueError("api_base is required") + return api_base + + @abstractmethod + def transform_video_create_request( + self, + model: str, + prompt: str, + video_create_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[Dict, RequestFiles]: + pass + + @abstractmethod + def transform_video_create_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoResponse: + pass + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from ..chat.transformation import BaseLLMException + + raise BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index d7b7987b6707..b71e37d8d698 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -44,6 +44,8 @@ from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig +from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig +from litellm.llms.base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig from litellm.llms.custom_httpx.http_handler import ( AsyncHTTPHandler, HTTPHandler, @@ -81,6 +83,7 @@ VectorStoreSearchOptionalRequestParams, VectorStoreSearchResponse, ) +from litellm.types.videos.main import VideoResponse from litellm.utils import ( CustomStreamWrapper, ImageResponse, @@ -3280,6 +3283,7 @@ def _handle_error( BaseAnthropicMessagesConfig, BaseBatchesConfig, BaseOCRConfig, + BaseVideoGenerationConfig, "BasePassthroughConfig", ], ): @@ -3782,6 +3786,369 @@ async def async_image_generation_handler( return model_response + ###### VIDEO GENERATION HANDLER ###### + def video_generation_handler( + self, + model: str, + prompt: str, + video_generation_provider_config: "BaseVideoGenerationConfig", + video_generation_optional_request_params: Dict, + custom_llm_provider: str, + litellm_params: GenericLiteLLMParams, + logging_obj: LiteLLMLoggingObj, + timeout: Union[float, httpx.Timeout], + extra_headers: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + _is_async: bool = False, + fake_stream: bool = False, + litellm_metadata: Optional[Dict[str, Any]] = None, + api_key: Optional[str] = None, + ) -> Union[ + VideoResponse, + Coroutine[Any, Any, VideoResponse], + ]: + """ + Handles video generation requests. + When _is_async=True, returns a coroutine instead of making the call directly. + """ + if _is_async: + # Return the async coroutine if called with _is_async=True + return self.async_video_generation_handler( + model=model, + prompt=prompt, + video_generation_provider_config=video_generation_provider_config, + video_generation_optional_request_params=video_generation_optional_request_params, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout, + client=client if isinstance(client, AsyncHTTPHandler) else None, + fake_stream=fake_stream, + litellm_metadata=litellm_metadata, + api_key=api_key, + ) + + if client is None or not isinstance(client, HTTPHandler): + sync_httpx_client = _get_httpx_client( + params={"ssl_verify": litellm_params.get("ssl_verify", None)} + ) + else: + sync_httpx_client = client + + headers = video_generation_provider_config.validate_environment( + api_key=api_key, + headers=video_generation_optional_request_params.get("extra_headers", {}) + or {}, + model=model, + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = video_generation_provider_config.get_complete_url( + model=model, + api_base=litellm_params.get("api_base", None), + litellm_params=dict(litellm_params), + ) + + data, files = video_generation_provider_config.transform_video_create_request( + model=model, + prompt=prompt, + video_create_optional_request_params=video_generation_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + ## LOGGING + logging_obj.pre_call( + input=prompt, + api_key="", + additional_args={ + "complete_input_dict": data, + "api_base": api_base, + "headers": headers, + }, + ) + + try: + # Use JSON when no files, otherwise use form data with files + if files is None or len(files) == 0: + # --- BEGIN MOCK VIDEO RESPONSE --- + mock_video_response = { + "data": [ + { + "id": "video_123", + "object": "video", + "model": "sora-2", + "status": "queued", + "progress": 0, + "created_at": 1712697600, + "size": "1024x1808", + "seconds": "8", + "quality": "standard", + } + ], + "usage": {}, + "hidden_params": {}, + } + + import types + class MockHTTPXResponse: + def __init__(self, json_data): + self._json_data = json_data + self.status_code = 200 + self.text = str(json_data) + def json(self): + return self._json_data + response = MockHTTPXResponse(mock_video_response) + # --- END MOCK VIDEO RESPONSE --- + else: + print(f"DEBUG: Using multipart form data request") + response = sync_httpx_client.post( + url=api_base, + headers=headers, + data=data, + files=files, + timeout=timeout, + ) + + except Exception as e: + raise self._handle_error( + e=e, + provider_config=video_generation_provider_config, + ) + + return video_generation_provider_config.transform_video_create_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + + async def async_video_generation_handler( + self, + model: str, + prompt: str, + video_generation_provider_config: "BaseVideoGenerationConfig", + video_generation_optional_request_params: Dict, + custom_llm_provider: str, + litellm_params: GenericLiteLLMParams, + logging_obj: LiteLLMLoggingObj, + timeout: Union[float, httpx.Timeout], + extra_headers: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + fake_stream: bool = False, + litellm_metadata: Optional[Dict[str, Any]] = None, + api_key: Optional[str] = None, + ) -> VideoResponse: + """ + Async version of the video generation handler. + Uses async HTTP client to make requests. + """ + if client is None or not isinstance(client, AsyncHTTPHandler): + async_httpx_client = get_async_httpx_client( + llm_provider=litellm.LlmProviders(custom_llm_provider), + params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + ) + else: + async_httpx_client = client + + headers = video_generation_provider_config.validate_environment( + api_key=api_key, + headers=video_generation_optional_request_params.get("extra_headers", {}) + or {}, + model=model, + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = video_generation_provider_config.get_complete_url( + model=model, + api_base=litellm_params.get("api_base", None), + litellm_params=dict(litellm_params), + ) + + data, files = video_generation_provider_config.transform_video_create_request( + model=model, + prompt=prompt, + video_create_optional_request_params=video_generation_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + ## LOGGING + logging_obj.pre_call( + input=prompt, + api_key="", + additional_args={ + "complete_input_dict": data, + "api_base": api_base, + "headers": headers, + }, + ) + + try: + # Use JSON when no files, otherwise use form data with files + if files is None or len(files) == 0: + response = await async_httpx_client.post( + url=api_base, + headers=headers, + json=data, + timeout=timeout, + ) + else: + response = await async_httpx_client.post( + url=api_base, + headers=headers, + data=data, + files=files, + timeout=timeout, + ) + + except Exception as e: + raise self._handle_error( + e=e, + provider_config=video_generation_provider_config, + ) + + return video_generation_provider_config.transform_video_create_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + + ###### VIDEO CONTENT HANDLER ###### + def video_content_handler( + self, + video_id: str, + video_content_provider_config: "BaseVideoRetrievalConfig", + custom_llm_provider: str, + litellm_params: GenericLiteLLMParams, + logging_obj: LiteLLMLoggingObj, + timeout: Union[float, httpx.Timeout], + extra_headers: Optional[Dict[str, Any]] = None, + api_key: Optional[str] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + variant: Optional[str] = None, + ) -> bytes: + """ + Handle video content download requests. + """ + if client is None or not isinstance(client, HTTPHandler): + sync_httpx_client = _get_httpx_client( + params={"ssl_verify": litellm_params.get("ssl_verify", None)} + ) + else: + sync_httpx_client = client + + headers = video_content_provider_config.validate_environment( + api_key=api_key, + headers=extra_headers or {}, + model="", # No model needed for content download + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = video_content_provider_config.get_complete_url( + model="", # No model needed for content download + api_base=litellm_params.get("api_base", None), + litellm_params=dict(litellm_params), + ) + + # Construct the URL for video content download + url = f"{api_base.rstrip('/')}/{video_id}/content" + + # Add variant query parameter if provided + params = {} + if variant: + params["variant"] = variant + + try: + # Make the GET request to download content + response = sync_httpx_client.get( + url=url, + headers=headers, + params=params, + ) + + # Return the raw content as bytes + return response.content + + except Exception as e: + raise self._handle_error( + e=e, + provider_config=video_content_provider_config, + ) + + async def async_video_content_handler( + self, + video_id: str, + video_content_provider_config: "BaseVideoRetrievalConfig", + custom_llm_provider: str, + litellm_params: GenericLiteLLMParams, + logging_obj: LiteLLMLoggingObj, + timeout: Union[float, httpx.Timeout], + extra_headers: Optional[Dict[str, Any]] = None, + api_key: Optional[str] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + variant: Optional[str] = None, + ) -> bytes: + """ + Async version of the video content download handler. + """ + if client is None or not isinstance(client, AsyncHTTPHandler): + async_httpx_client = get_async_httpx_client( + llm_provider=litellm.LlmProviders(custom_llm_provider), + params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + ) + else: + async_httpx_client = client + + headers = video_content_provider_config.validate_environment( + api_key=api_key, + headers=extra_headers or {}, + model="", # No model needed for content download + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = video_content_provider_config.get_complete_url( + model="", # No model needed for content download + api_base=litellm_params.get("api_base", None), + litellm_params=dict(litellm_params), + ) + + # Construct the URL for video content download + url = f"{api_base.rstrip('/')}/{video_id}/content" + + # Add variant query parameter if provided + params = {} + if variant: + params["variant"] = variant + + try: + # Make the GET request to download content + response = await async_httpx_client.get( + url=url, + headers=headers, + params=params, + ) + + # Return the raw content as bytes + return response.content + + except Exception as e: + raise self._handle_error( + e=e, + provider_config=video_content_provider_config, + ) + ###### VECTOR STORE HANDLER ###### async def async_vector_store_search_handler( self, diff --git a/litellm/llms/openai/video_generation/transformation.py b/litellm/llms/openai/video_generation/transformation.py new file mode 100644 index 000000000000..a610f608125d --- /dev/null +++ b/litellm/llms/openai/video_generation/transformation.py @@ -0,0 +1,182 @@ +import types +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from io import BufferedReader + +import httpx +from httpx._types import RequestFiles + +from litellm.types.videos.main import VideoCreateOptionalRequestParams +from litellm.types.llms.openai import CreateVideoRequest, OpenAIVideoObject +from litellm.types.videos.main import VideoResponse +from litellm.types.router import GenericLiteLLMParams + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from litellm.types.videos.main import VideoResponse as _VideoResponse + + from ...base_llm.videos_generation.transformation import BaseVideoGenerationConfig as _BaseVideoGenerationConfig + from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseVideoGenerationConfig = _BaseVideoGenerationConfig + BaseLLMException = _BaseLLMException + VideoResponse = _VideoResponse +else: + LiteLLMLoggingObj = Any + BaseVideoGenerationConfig = Any + BaseLLMException = Any + VideoResponse = Any + + +class OpenAIVideoGenerationConfig(BaseVideoGenerationConfig): + """ + Configuration class for OpenAI video generation. + """ + + def __init__(self): + super().__init__() + + def get_supported_openai_params(self, model: str) -> list: + """ + Get the list of supported OpenAI parameters for video generation. + """ + return [ + "model", + "prompt", + "input_reference", + "seconds", + "size", + "user", + "extra_headers", + ] + + def map_openai_params( + self, + video_create_optional_params: VideoCreateOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + """No mapping applied since inputs are in OpenAI spec already""" + return dict(video_create_optional_params) + + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + """ + Validate the environment for OpenAI video generation. + """ + if api_key is None: + raise ValueError("OpenAI API key is required for video generation") + + headers["Authorization"] = f"Bearer {api_key}" + headers["Content-Type"] = "application/json" + + return headers + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for OpenAI video generation. + """ + if api_base is None: + api_base = "https://api.openai.com/v1" + + return f"{api_base.rstrip('/')}/videos" + + def transform_video_create_request( + self, + model: str, + prompt: str, + video_create_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[Dict, RequestFiles]: + """ + Transform the video creation request for OpenAI API. + """ + # Remove model and extra_headers from optional params as they're handled separately + video_create_optional_request_params = { + k: v for k, v in video_create_optional_request_params.items() + if k not in ["model", "extra_headers"] + } + + # Create the request data + video_create_request = CreateVideoRequest( + model=model, + prompt=prompt, + **video_create_optional_request_params + ) + + # Handle file uploads + files_list: RequestFiles = [] + + # Handle input_reference parameter if provided + _input_reference = video_create_optional_request_params.get("input_reference") + if _input_reference is not None: + if isinstance(_input_reference, BufferedReader): + files_list.append( + ("input_reference", (_input_reference.name, _input_reference, "image/png")) + ) + elif isinstance(_input_reference, str): + # Handle file path - open the file + try: + with open(_input_reference, "rb") as f: + files_list.append( + ("input_reference", (f.name, f.read(), "image/png")) + ) + except Exception as e: + raise ValueError(f"Could not open input_reference file {_input_reference}: {e}") + else: + # Handle file-like object + files_list.append( + ("input_reference", ("input_reference.png", _input_reference, "image/png")) + ) + + # Convert to dict for JSON serialization + data = video_create_request.model_dump(exclude_none=True) + + return data, files_list + + def transform_video_create_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoResponse: + """ + Transform the OpenAI video creation response. + """ + response_data = raw_response.json() + + # Transform the response data + video_objects = [] + for video_data in response_data.get("data", []): + video_obj = OpenAIVideoObject(**video_data) + video_objects.append(video_obj) + + # Create the response + response = VideoResponse( + data=video_objects, + usage=response_data.get("usage", {}), + hidden_params={}, + ) + + return response + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from ...base_llm.chat.transformation import BaseLLMException + + raise BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) diff --git a/litellm/llms/openai/video_retrieval/transformation.py b/litellm/llms/openai/video_retrieval/transformation.py new file mode 100644 index 000000000000..80fc95544d6b --- /dev/null +++ b/litellm/llms/openai/video_retrieval/transformation.py @@ -0,0 +1,99 @@ +import types +from typing import TYPE_CHECKING, Any, Dict, Optional, Union + +import httpx + +from litellm.types.llms.openai import OpenAIVideoObject +from litellm.types.videos.main import VideoResponse +from litellm.types.router import GenericLiteLLMParams + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from litellm.types.videos.main import VideoResponse as _VideoResponse + + from ...base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig as _BaseVideoRetrievalConfig + from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseVideoRetrievalConfig = _BaseVideoRetrievalConfig + BaseLLMException = _BaseLLMException + VideoResponse = _VideoResponse +else: + LiteLLMLoggingObj = Any + BaseVideoRetrievalConfig = Any + BaseLLMException = Any + VideoResponse = Any + + +class OpenAIVideoRetrievalConfig(BaseVideoRetrievalConfig): + """ + Configuration class for OpenAI video retrieval operations. + """ + + def __init__(self): + super().__init__() + + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + """ + Validate the environment for OpenAI video retrieval. + """ + if api_key is None: + raise ValueError("OpenAI API key is required for video retrieval") + + headers["Authorization"] = f"Bearer {api_key}" + headers["Content-Type"] = "application/json" + + return headers + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for OpenAI video retrieval. + """ + if api_base is None: + api_base = "https://api.openai.com/v1" + + return f"{api_base.rstrip('/')}/videos" + + def transform_video_retrieve_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoResponse: + """ + Transform the OpenAI video retrieval response. + """ + response_data = raw_response.json() + + # Transform the response data + video_obj = OpenAIVideoObject(**response_data) + + # Create the response + response = VideoResponse( + data=[video_obj], + usage={}, + hidden_params={}, + ) + + return response + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from ...base_llm.chat.transformation import BaseLLMException + + raise BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index c6f8275a6a12..cd9c9a1e3663 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -23724,5 +23724,12 @@ "supports_tool_choice": true, "supports_vision": true, "supports_web_search": true + }, + "sora-2": { + "input_cost_per_second": 0.0, + "litellm_provider": "openai", + "mode": "video_generation", + "output_cost_per_second": 0.0, + "supports_video_generation": true } } diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index a48d9a29911a..31fe74df5cd1 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1831,3 +1831,88 @@ class OpenAIMcpServerTool(TypedDict, total=False): require_approval: str allowed_tools: Optional[List[str]] headers: Optional[Dict[str, str]] + + +# Video Generation Types +class CreateVideoRequest(TypedDict, total=False): + """ + CreateVideoRequest for OpenAI video generation API + + Required Params: + prompt: str - Text prompt that describes the video to generate + + Optional Params: + input_reference: Optional[str] - Optional image reference that guides generation + model: Optional[str] - The video generation model to use (defaults to sora-2) + seconds: Optional[str] - Clip duration in seconds (defaults to 4 seconds) + size: Optional[str] - Output resolution formatted as width x height (defaults to 720x1280) + user: Optional[str] - A unique identifier representing your end-user + extra_headers: Optional[Dict[str, str]] - Additional headers + extra_body: Optional[Dict[str, str]] - Additional body parameters + timeout: Optional[float] - Request timeout + """ + prompt: Required[str] + input_reference: Optional[str] + model: Optional[str] + seconds: Optional[str] + size: Optional[str] + user: Optional[str] + extra_headers: Optional[Dict[str, str]] + extra_body: Optional[Dict[str, str]] + timeout: Optional[float] + + +class OpenAIVideoObject(BaseModel): + """OpenAI Video Object representing a video generation job.""" + id: str + """Unique identifier for the video job.""" + + object: Literal["video"] + """The object type, which is always 'video'.""" + + status: str + """Current lifecycle status of the video job.""" + + created_at: int + """Unix timestamp (seconds) for when the job was created.""" + + completed_at: Optional[int] = None + """Unix timestamp (seconds) for when the job completed, if finished.""" + + expires_at: Optional[int] = None + """Unix timestamp (seconds) for when the downloadable assets expire, if set.""" + + error: Optional[Dict[str, Any]] = None + """Error payload that explains why generation failed, if applicable.""" + + progress: Optional[int] = None + """Approximate completion percentage for the generation task.""" + + remixed_from_video_id: Optional[str] = None + """Identifier of the source video if this video is a remix.""" + + seconds: Optional[str] = None + """Duration of the generated clip in seconds.""" + + size: Optional[str] = None + """The resolution of the generated video.""" + + model: Optional[str] = None + """The video generation model that produced the job.""" + + _hidden_params: Dict[str, Any] = {} + + def __contains__(self, key): + return hasattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + def __getitem__(self, key): + return getattr(self, key) + + def json(self, **kwargs): # type: ignore + try: + return self.model_dump(**kwargs) + except Exception: + return self.dict() diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d744bb9d38b5..f1eeee4cbbd5 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -276,6 +276,20 @@ class CallTypes(str, Enum): file_content = "file_content" create_fine_tuning_job = "create_fine_tuning_job" acreate_fine_tuning_job = "acreate_fine_tuning_job" + + ######################################################### + # Video Generation Call Types + ######################################################### + create_video = "create_video" + acreate_video = "acreate_video" + avideo_retrieve = "avideo_retrieve" + video_retrieve = "video_retrieve" + avideo_delete = "avideo_delete" + video_delete = "video_delete" + avideo_list = "avideo_list" + video_list = "video_list" + avideo_content = "avideo_content" + video_content = "video_content" acancel_fine_tuning_job = "acancel_fine_tuning_job" cancel_fine_tuning_job = "cancel_fine_tuning_job" alist_fine_tuning_jobs = "alist_fine_tuning_jobs" diff --git a/litellm/types/videos/main.py b/litellm/types/videos/main.py new file mode 100644 index 000000000000..30d0b4912db5 --- /dev/null +++ b/litellm/types/videos/main.py @@ -0,0 +1,112 @@ +from typing import Any, Dict, List, Literal, Optional, Union +from typing_extensions import TypedDict + +from pydantic import BaseModel +from litellm.types.utils import FileTypes + + +class VideoObject(BaseModel): + """Represents a generated video object.""" + id: str + object: Literal["video"] + status: str + created_at: int + completed_at: Optional[int] = None + expires_at: Optional[int] = None + error: Optional[Dict[str, Any]] = None + progress: Optional[int] = None + remixed_from_video_id: Optional[str] = None + seconds: Optional[str] = None + size: Optional[str] = None + model: Optional[str] = None + _hidden_params: Dict[str, Any] = {} + + def __contains__(self, key): + # Define custom behavior for the 'in' operator + return hasattr(self, key) + + def get(self, key, default=None): + # Custom .get() method to access attributes with a default value if the attribute doesn't exist + return getattr(self, key, default) + + def __getitem__(self, key): + # Allow dictionary-style access to attributes + return getattr(self, key) + + def json(self, **kwargs): # type: ignore + try: + return self.model_dump(**kwargs) + except Exception: + # if using pydantic v1 + return self.dict() + + +class VideoUsage(BaseModel): + """Usage information for video generation.""" + input_tokens: Optional[int] = None + output_tokens: Optional[int] = None + total_tokens: Optional[int] = None + duration_seconds: Optional[float] = None + resolution: Optional[str] = None + _hidden_params: Dict[str, Any] = {} + + def __contains__(self, key): + return hasattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + def __getitem__(self, key): + return getattr(self, key) + + def json(self, **kwargs): # type: ignore + try: + return self.model_dump(**kwargs) + except Exception: + return self.dict() + + +class VideoResponse(BaseModel): + """Response object for video generation requests.""" + data: List[VideoObject] + usage: VideoUsage + hidden_params: Dict[str, Any] = {} + + def __contains__(self, key): + return hasattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + def __getitem__(self, key): + return getattr(self, key) + + def json(self, **kwargs): # type: ignore + try: + return self.model_dump(**kwargs) + except Exception: + return self.dict() + + +class VideoCreateOptionalRequestParams(TypedDict, total=False): + """ + TypedDict for Optional parameters supported by OpenAI's video creation API. + + Params here: https://platform.openai.com/docs/api-reference/videos/create + """ + input_reference: Optional[FileTypes] # File reference for input image + model: Optional[str] + seconds: Optional[str] + size: Optional[str] + user: Optional[str] + extra_headers: Optional[Dict[str, str]] + extra_body: Optional[Dict[str, str]] + + +class VideoCreateRequestParams(VideoCreateOptionalRequestParams, total=False): + """ + TypedDict for request parameters supported by OpenAI's video creation API. + + Params here: https://platform.openai.com/docs/api-reference/videos/create + """ + prompt: str diff --git a/litellm/utils.py b/litellm/utils.py index e598213f0aa8..ce0c00bf3652 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -262,6 +262,7 @@ from litellm.llms.base_llm.image_variations.transformation import ( BaseImageVariationConfig, ) +from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig @@ -7551,6 +7552,19 @@ def get_provider_image_generation_config( return LiteLLMProxyImageGenerationConfig() return None + @staticmethod + def get_provider_video_generation_config( + model: str, + provider: LlmProviders, + ) -> Optional[BaseVideoGenerationConfig]: + if LlmProviders.OPENAI == provider: + from litellm.llms.openai.video_generation.transformation import ( + OpenAIVideoGenerationConfig, + ) + + return OpenAIVideoGenerationConfig() + return None + @staticmethod def get_provider_realtime_config( model: str, diff --git a/litellm/videos/main.py b/litellm/videos/main.py new file mode 100644 index 000000000000..5aa65d4164f2 --- /dev/null +++ b/litellm/videos/main.py @@ -0,0 +1,767 @@ +import asyncio +import contextvars +import time +from functools import partial +from typing import Any, Coroutine, Literal, Optional, Union, overload, Dict + +import litellm +import orjson +from litellm._logging import verbose_logger +from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider +from litellm.types.videos.main import ( + VideoCreateOptionalRequestParams, + VideoObject, + VideoResponse, + VideoUsage, +) +from litellm.videos.utils import VideoGenerationRequestUtils +from litellm.constants import DEFAULT_VIDEO_ENDPOINT_MODEL, request_timeout as DEFAULT_REQUEST_TIMEOUT +from litellm.main import base_llm_http_handler +from litellm.types.utils import all_litellm_params, LlmProviders, CallTypes +from litellm.utils import client, exception_type, ProviderConfigManager +from litellm.types.utils import FileTypes +from litellm.types.router import GenericLiteLLMParams +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig +from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler +from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler +from litellm.llms.custom_llm import CustomLLM + +#################### Initialize provider clients #################### +llm_http_handler: BaseLLMHTTPHandler = BaseLLMHTTPHandler() + +##### Video Generation ####################### +@client +async def avideo_generation(*args, **kwargs) -> VideoResponse: + """ + Asynchronously calls the `video_generation` function with the given arguments and keyword arguments. + + Parameters: + - `args` (tuple): Positional arguments to be passed to the `video_generation` function. + - `kwargs` (dict): Keyword arguments to be passed to the `video_generation` function. + + Returns: + - `response` (VideoResponse): The response returned by the `video_generation` function. + """ + loop = asyncio.get_event_loop() + model = args[0] if len(args) > 0 else kwargs["model"] + ### PASS ARGS TO Video Generation ### + kwargs["avideo_generation"] = True + custom_llm_provider = None + try: + # Check for mock response first + mock_response = kwargs.get("mock_response", None) + if mock_response is not None: + if isinstance(mock_response, str): + mock_response = orjson.loads(mock_response) + + response = VideoResponse( + data=[VideoObject(**video_data) for video_data in mock_response.get("data", [])], + usage=VideoUsage(**mock_response.get("usage", {})), + hidden_params=kwargs.get("hidden_params", {}), + ) + return response + + # Use a partial function to pass your keyword arguments + func = partial(video_generation, *args, **kwargs) + + # Add the context to the function + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + + _, custom_llm_provider, _, _ = get_llm_provider( + model=model, api_base=kwargs.get("api_base", None) + ) + + # Await normally + init_response = await loop.run_in_executor(None, func_with_context) + + response: Optional[VideoResponse] = None + if isinstance(init_response, dict): + response = VideoResponse(**init_response) + elif isinstance(init_response, VideoResponse): ## CACHING SCENARIO + response = init_response + elif asyncio.iscoroutine(init_response): + response = await init_response # type: ignore + + if response is None: + raise ValueError( + "Unable to get Video Response. Please pass a valid llm_provider." + ) + + return response + except Exception as e: + custom_llm_provider = custom_llm_provider or "openai" + raise exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=args, + extra_kwargs=kwargs, + ) + + +# fmt: off + +# Overload for when avideo_generation=True (returns Coroutine) +@overload +def video_generation( + prompt: str, + model: Optional[str] = None, + input_reference: Optional[str] = None, + size: Optional[str] = None, + user: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_generation: Literal[True], + **kwargs, +) -> Coroutine[Any, Any, VideoResponse]: + ... + + +@overload +def video_generation( + prompt: str, + model: Optional[str] = None, + input_reference: Optional[str] = None, + seconds: Optional[str] = None, + size: Optional[str] = None, + user: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_generation: Literal[False] = False, + **kwargs, +) -> VideoResponse: + ... + +# fmt: on + + +@client +def video_generation( # noqa: PLR0915 + prompt: str, + model: Optional[str] = None, + input_reference: Optional[FileTypes] = None, + seconds: Optional[str] = None, + size: Optional[str] = None, + user: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> Union[ + VideoResponse, + Coroutine[Any, Any, VideoResponse], +]: + """ + Maps the https://api.openai.com/v1/videos endpoint. + + Currently supports OpenAI + """ + local_vars = locals() + try: + litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore + litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None) + _is_async = kwargs.pop("async_call", False) is True + + # Check for mock response first + mock_response = kwargs.get("mock_response", None) + if mock_response is not None: + if isinstance(mock_response, str): + mock_response = orjson.loads(mock_response) + + response = VideoResponse( + data=[VideoObject(**video_data) for video_data in mock_response.get("data", [])], + usage=VideoUsage(**mock_response.get("usage", {})), + hidden_params=kwargs.get("hidden_params", {}), + ) + return response + + # get llm provider logic + litellm_params = GenericLiteLLMParams(**kwargs) + model, custom_llm_provider, _, _ = get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, + custom_llm_provider=custom_llm_provider, + ) + + # get provider config + video_generation_provider_config: Optional[BaseVideoGenerationConfig] = ( + ProviderConfigManager.get_provider_video_generation_config( + model=model, + provider=litellm.LlmProviders(custom_llm_provider), + ) + ) + + if video_generation_provider_config is None: + raise ValueError(f"image edit is not supported for {custom_llm_provider}") + + local_vars.update(kwargs) + # Get ImageEditOptionalRequestParams with only valid parameters + video_generation_optional_params: VideoCreateOptionalRequestParams = ( + VideoGenerationRequestUtils.get_requested_video_generation_optional_param(local_vars) + ) + + # Get optional parameters for the responses API + video_generation_request_params: Dict = ( + VideoGenerationRequestUtils.get_optional_params_video_generation( + model=model, + video_generation_provider_config=video_generation_provider_config, + video_generation_optional_params=video_generation_optional_params, + ) + ) + + # Pre Call logging + litellm_logging_obj.update_environment_variables( + model=model, + user=user, + optional_params=dict(video_generation_request_params), + litellm_params={ + "litellm_call_id": litellm_call_id, + **video_generation_request_params, + }, + custom_llm_provider=custom_llm_provider, + ) + + # Call the handler with _is_async flag instead of directly calling the async handler + return base_llm_http_handler.video_generation_handler( + model=model, + prompt=prompt, + video_generation_provider_config=video_generation_provider_config, + video_generation_optional_request_params=video_generation_request_params, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=litellm_logging_obj, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout or DEFAULT_REQUEST_TIMEOUT, + _is_async=_is_async, + client=kwargs.get("client"), + ) + + except Exception as e: + raise litellm.exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +##### Video Retrieval ####################### +@client +async def avideo_retrieve(*args, **kwargs) -> VideoResponse: + """ + Asynchronously retrieve a video generation job status. + """ + loop = asyncio.get_event_loop() + video_id = args[0] if len(args) > 0 else kwargs["video_id"] + + kwargs["avideo_retrieve"] = True + custom_llm_provider = None + try: + # Check for mock response first + mock_response = kwargs.get("mock_response", None) + if mock_response is not None: + if isinstance(mock_response, str): + mock_response = orjson.loads(mock_response) + + response = VideoResponse( + data=[VideoObject(**video_data) for video_data in mock_response.get("data", [])], + usage=VideoUsage(**mock_response.get("usage", {})), + hidden_params=kwargs.get("hidden_params", {}), + ) + return response + + # Use a partial function to pass your keyword arguments + func = partial(video_retrieve, *args, **kwargs) + + # Add the context to the function + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + + _, custom_llm_provider, _, _ = get_llm_provider( + model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) + ) + + # Await normally + init_response = await loop.run_in_executor(None, func_with_context) + + response: Optional[VideoResponse] = None + if isinstance(init_response, dict): + response = VideoResponse(**init_response) + elif isinstance(init_response, VideoResponse): ## CACHING SCENARIO + response = init_response + elif asyncio.iscoroutine(init_response): + response = await init_response # type: ignore + + if response is None: + raise ValueError( + "Unable to get Video Response. Please pass a valid llm_provider." + ) + + return response + except Exception as e: + custom_llm_provider = custom_llm_provider or "openai" + raise exception_type( + model=kwargs.get("model", "sora-2"), + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=args, + extra_kwargs=kwargs, + ) + + +@client +def video_retrieve( + video_id: str, + model: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[float] = None, + custom_llm_provider: Optional[str] = None, + **kwargs, +) -> VideoResponse: + """ + Retrieve a video generation job status. + """ + try: + if custom_llm_provider is None: + custom_llm_provider = "openai" + + # For now, return a placeholder response since video retrieval is not yet implemented + verbose_logger.warning( + f"Video retrieval for provider {custom_llm_provider} is not yet implemented." + ) + + model_response = VideoResponse( + data=[ + VideoObject( + id=video_id, + object="video", + status="completed", + created_at=int(time.time()), + completed_at=int(time.time()), + model=model or "sora-2" + ) + ], + usage=VideoUsage(), + hidden_params=kwargs.get("hidden_params", {}), + ) + + return model_response + + except Exception as e: + verbose_logger.error(f"Error in video_retrieve: {e}") + raise e + + +##### Video Deletion ####################### +@client +async def avideo_delete(*args, **kwargs) -> bool: + """ + Asynchronously delete a video generation job. + """ + loop = asyncio.get_event_loop() + video_id = args[0] if len(args) > 0 else kwargs["video_id"] + + kwargs["avideo_delete"] = True + custom_llm_provider = None + try: + # Use a partial function to pass your keyword arguments + func = partial(video_delete, *args, **kwargs) + + # Add the context to the function + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + + _, custom_llm_provider, _, _ = get_llm_provider( + model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) + ) + + # Await normally + result = await loop.run_in_executor(None, func_with_context) + return result + except Exception as e: + custom_llm_provider = custom_llm_provider or "openai" + raise exception_type( + model=kwargs.get("model", "sora-2"), + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=args, + extra_kwargs=kwargs, + ) + + +@client +def video_delete( + video_id: str, + model: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[float] = None, + custom_llm_provider: Optional[str] = None, + **kwargs, +) -> bool: + """ + Delete a video generation job. + """ + try: + if custom_llm_provider is None: + custom_llm_provider = "openai" + + # For now, return a placeholder response since video deletion is not yet implemented + verbose_logger.warning( + f"Video deletion for provider {custom_llm_provider} is not yet implemented." + ) + result = True + + return result + + except Exception as e: + verbose_logger.error(f"Error in video_delete: {e}") + raise e + + +##### Video Content Download ####################### +@client +async def avideo_content(*args, **kwargs) -> bytes: + """ + Asynchronously download video content. + """ + loop = asyncio.get_event_loop() + video_id = args[0] if len(args) > 0 else kwargs["video_id"] + + kwargs["avideo_content"] = True + custom_llm_provider = None + try: + # Use a partial function to pass your keyword arguments + func = partial(video_content, *args, **kwargs) + + # Add the context to the function + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + + _, custom_llm_provider, _, _ = get_llm_provider( + model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) + ) + + # Await normally + result = await loop.run_in_executor(None, func_with_context) + return result + except Exception as e: + custom_llm_provider = custom_llm_provider or "openai" + raise exception_type( + model=kwargs.get("model", "sora-2"), + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=args, + extra_kwargs=kwargs, + ) + + +@client +def video_content( + video_id: str, + model: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[float] = None, + custom_llm_provider: Optional[str] = None, + **kwargs, +) -> bytes: + """ + Download video content. + """ + try: + if custom_llm_provider is None: + custom_llm_provider = "openai" + + # For now, return placeholder content + verbose_logger.warning( + f"Video content download for provider {custom_llm_provider} is not yet implemented." + ) + + # Return placeholder video content + return b"placeholder video content" + + except Exception as e: + verbose_logger.error(f"Error in video_content: {e}") + raise e + + +##### Video Listing ####################### +@client +async def avideo_list(*args, **kwargs) -> VideoResponse: + """ + Asynchronously list video generation jobs. + """ + loop = asyncio.get_event_loop() + + kwargs["avideo_list"] = True + custom_llm_provider = None + try: + # Use a partial function to pass your keyword arguments + func = partial(video_list, *args, **kwargs) + + # Add the context to the function + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + + _, custom_llm_provider, _, _ = get_llm_provider( + model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) + ) + + # Await normally + init_response = await loop.run_in_executor(None, func_with_context) + + response: Optional[VideoResponse] = None + if isinstance(init_response, dict): + response = VideoResponse(**init_response) + elif isinstance(init_response, VideoResponse): ## CACHING SCENARIO + response = init_response + elif asyncio.iscoroutine(init_response): + response = await init_response # type: ignore + + if response is None: + raise ValueError( + "Unable to get Video Response. Please pass a valid llm_provider." + ) + + return response + except Exception as e: + custom_llm_provider = custom_llm_provider or "openai" + raise exception_type( + model=kwargs.get("model", "sora-2"), + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=args, + extra_kwargs=kwargs, + ) + + +@client +def video_list( + model: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[float] = None, + custom_llm_provider: Optional[str] = None, + **kwargs, +) -> VideoResponse: + """ + List video generation jobs. + """ + try: + if custom_llm_provider is None: + custom_llm_provider = "openai" + + # For now, return placeholder response + verbose_logger.warning( + f"Video listing for provider {custom_llm_provider} is not yet implemented." + ) + + model_response = VideoResponse( + data=[], + usage=VideoUsage(), + hidden_params=kwargs.get("hidden_params", {}), + ) + + return model_response + + except Exception as e: + verbose_logger.error(f"Error in video_list: {e}") + raise e + + +# Convenience functions with better names +create_video = video_generation +acreate_video = avideo_generation + + +def video_content( + video_id: str, + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[float] = None, + extra_headers: Optional[Dict[str, Any]] = None, + litellm_params: Optional[Dict[str, Any]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + variant: Optional[str] = None, +) -> bytes: + """ + Download video content from OpenAI's video API. + + Args: + video_id (str): The identifier of the video whose content to download. + custom_llm_provider (Optional[str]): The LLM provider to use. If not provided, will be auto-detected. + api_key (Optional[str]): The API key to use for authentication. + api_base (Optional[str]): The base URL for the API. + timeout (Optional[float]): The timeout for the request in seconds. + extra_headers (Optional[Dict[str, Any]]): Additional headers to include in the request. + litellm_params (Optional[Dict[str, Any]]): Additional LiteLLM parameters. + client (Optional[Union[HTTPHandler, AsyncHTTPHandler]]): The HTTP client to use. + variant (Optional[str]): Which downloadable asset to return. Defaults to the MP4 video. + + Returns: + bytes: The raw video content as bytes. + + Example: + ```python + import litellm + + # Download video content + video_bytes = litellm.video_content( + video_id="video_123", + custom_llm_provider="openai" + ) + + # Save to file + with open("video.mp4", "wb") as f: + f.write(video_bytes) + ``` + """ + if litellm_params is None: + litellm_params = {} + + # Set default timeout if not provided + if timeout is None: + timeout = DEFAULT_REQUEST_TIMEOUT + + # Get the LLM provider if not provided + if custom_llm_provider is None: + custom_llm_provider = "openai" # Default to OpenAI for video content + + # Get the provider config + from litellm.llms.openai.video_retrieval.transformation import OpenAIVideoRetrievalConfig + video_content_provider_config = OpenAIVideoRetrievalConfig() + + # Create logging object + logging_obj = LiteLLMLoggingObj( + model="", # No model needed for content download + messages=[], # No messages for content download + stream=False, + call_type=CallTypes.video_content.value, + start_time=time.time(), + litellm_call_id="", + function_id="video_content", + ) + + # Get the HTTP handler + from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler + base_llm_http_handler = BaseLLMHTTPHandler() + + # Call the video content handler + return base_llm_http_handler.video_content_handler( + video_id=video_id, + video_content_provider_config=video_content_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + timeout=timeout, + extra_headers=extra_headers, + api_key=api_key, + client=client, + variant=variant, + ) + + +async def async_video_content( + video_id: str, + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[float] = None, + extra_headers: Optional[Dict[str, Any]] = None, + litellm_params: Optional[Dict[str, Any]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + variant: Optional[str] = None, +) -> bytes: + """ + Async version of video content download. + + Args: + video_id (str): The identifier of the video whose content to download. + custom_llm_provider (Optional[str]): The LLM provider to use. If not provided, will be auto-detected. + api_key (Optional[str]): The API key to use for authentication. + api_base (Optional[str]): The base URL for the API. + timeout (Optional[float]): The timeout for the request in seconds. + extra_headers (Optional[Dict[str, Any]]): Additional headers to include in the request. + litellm_params (Optional[Dict[str, Any]]): Additional LiteLLM parameters. + client (Optional[Union[HTTPHandler, AsyncHTTPHandler]]): The HTTP client to use. + variant (Optional[str]): Which downloadable asset to return. Defaults to the MP4 video. + + Returns: + bytes: The raw video content as bytes. + + Example: + ```python + import litellm + + # Download video content asynchronously + video_bytes = await litellm.async_video_content( + video_id="video_123", + custom_llm_provider="openai" + ) + + # Save to file + with open("video.mp4", "wb") as f: + f.write(video_bytes) + ``` + """ + if litellm_params is None: + litellm_params = {} + + # Set default timeout if not provided + if timeout is None: + timeout = DEFAULT_REQUEST_TIMEOUT + + # Get the LLM provider if not provided + if custom_llm_provider is None: + custom_llm_provider = "openai" # Default to OpenAI for video content + + # Get the provider config + from litellm.llms.openai.video_retrieval.transformation import OpenAIVideoRetrievalConfig + video_content_provider_config = OpenAIVideoRetrievalConfig() + + # Create logging object + logging_obj = LiteLLMLoggingObj( + model="", # No model needed for content download + messages=[], # No messages for content download + stream=False, + call_type=CallTypes.video_content.value, + start_time=time.time(), + litellm_call_id="", + function_id="async_video_content", + ) + + # Get the HTTP handler + from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler + base_llm_http_handler = BaseLLMHTTPHandler() + + # Call the async video content handler + return await base_llm_http_handler.async_video_content_handler( + video_id=video_id, + video_content_provider_config=video_content_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + timeout=timeout, + extra_headers=extra_headers, + api_key=api_key, + client=client, + variant=variant, + ) \ No newline at end of file diff --git a/litellm/videos/utils.py b/litellm/videos/utils.py new file mode 100644 index 000000000000..2e7e025927c0 --- /dev/null +++ b/litellm/videos/utils.py @@ -0,0 +1,71 @@ +from typing import Any, Dict, cast, get_type_hints + +import litellm +from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig +from litellm.types.videos.main import VideoCreateOptionalRequestParams + + +class VideoGenerationRequestUtils: + """Helper utils for constructing video generation requests""" + + @staticmethod + def get_optional_params_video_generation( + model: str, + video_generation_provider_config: BaseVideoGenerationConfig, + video_generation_optional_params: VideoCreateOptionalRequestParams, + ) -> Dict: + """ + Get optional parameters for the video generation API. + + Args: + model: The model name + video_generation_provider_config: The provider configuration for video generation API + video_generation_optional_params: The optional parameters for video generation + + Returns: + A dictionary of supported parameters for the video generation API + """ + # Get supported parameters for the model + supported_params = video_generation_provider_config.get_supported_openai_params(model) + + # Check for unsupported parameters + unsupported_params = [ + param + for param in video_generation_optional_params + if param not in supported_params + ] + + if unsupported_params: + raise litellm.UnsupportedParamsError( + model=model, + message=f"The following parameters are not supported for model {model}: {', '.join(unsupported_params)}", + ) + + # Map parameters to provider-specific format + mapped_params = video_generation_provider_config.map_openai_params( + video_create_optional_params=video_generation_optional_params, + model=model, + drop_params=litellm.drop_params, + ) + + return mapped_params + + @staticmethod + def get_requested_video_generation_optional_param( + params: Dict[str, Any], + ) -> VideoCreateOptionalRequestParams: + """ + Filter parameters to only include those defined in VideoCreateOptionalRequestParams. + + Args: + params: Dictionary of parameters to filter + + Returns: + VideoCreateOptionalRequestParams instance with only the valid parameters + """ + valid_keys = get_type_hints(VideoCreateOptionalRequestParams).keys() + filtered_params = { + k: v for k, v in params.items() if k in valid_keys and v is not None + } + + return cast(VideoCreateOptionalRequestParams, filtered_params) From 7377e2325d50b2667a156af8d12661e01203aa92 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 14 Oct 2025 22:42:31 +0530 Subject: [PATCH 02/17] add retrieval endpoint --- litellm/__init__.py | 4 +- .../video_retrieval/transformation.py | 5 +- litellm/llms/custom_httpx/llm_http_handler.py | 130 ++-- .../openai/video_generation/transformation.py | 35 +- .../openai/video_retrieval/transformation.py | 52 +- litellm/types/llms/openai.py | 28 +- litellm/types/utils.py | 15 +- litellm/types/videos/main.py | 2 +- litellm/utils.py | 14 + litellm/videos/main.py | 685 +++++------------- 10 files changed, 333 insertions(+), 637 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index 0965f5921b7c..d78ecc960258 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -104,7 +104,7 @@ # Register async client cleanup to prevent resource leaks register_async_client_cleanup() #################################################### -if set_verbose == True: +if set_verbose: _turn_on_debug() #################################################### ### Callbacks /Logging / Success / Failure Handlers ##### @@ -1209,7 +1209,6 @@ def add_known_models(): OpenAIOSeriesConfig, ) -from .llms.snowflake.chat.transformation import SnowflakeConfig from .llms.gradient_ai.chat.transformation import GradientAIConfig openaiOSeriesConfig = OpenAIOSeriesConfig() @@ -1245,7 +1244,6 @@ def add_known_models(): from .llms.baseten.chat import BasetenConfig from .llms.sambanova.chat import SambanovaConfig from .llms.sambanova.embedding.transformation import SambaNovaEmbeddingConfig -from .llms.ai21.chat.transformation import AI21ChatConfig from .llms.fireworks_ai.chat.transformation import FireworksAIConfig from .llms.fireworks_ai.completion.transformation import FireworksAITextCompletionConfig from .llms.fireworks_ai.audio_transcription.transformation import ( diff --git a/litellm/llms/base_llm/video_retrieval/transformation.py b/litellm/llms/base_llm/video_retrieval/transformation.py index db48d821444f..9cb10540eeda 100644 --- a/litellm/llms/base_llm/video_retrieval/transformation.py +++ b/litellm/llms/base_llm/video_retrieval/transformation.py @@ -1,10 +1,9 @@ import types from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union import httpx -from litellm.types.router import GenericLiteLLMParams if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -75,7 +74,7 @@ def transform_video_retrieve_response( model: str, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, - ) -> VideoResponse: + ) -> bytes: pass def get_error_class( diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index b71e37d8d698..04193d53690f 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -3284,6 +3284,7 @@ def _handle_error( BaseBatchesConfig, BaseOCRConfig, BaseVideoGenerationConfig, + "BaseVideoRetrievalConfig", "BasePassthroughConfig", ], ): @@ -3875,38 +3876,47 @@ def video_generation_handler( try: # Use JSON when no files, otherwise use form data with files - if files is None or len(files) == 0: - # --- BEGIN MOCK VIDEO RESPONSE --- - mock_video_response = { - "data": [ - { - "id": "video_123", - "object": "video", - "model": "sora-2", - "status": "queued", - "progress": 0, - "created_at": 1712697600, - "size": "1024x1808", - "seconds": "8", - "quality": "standard", - } - ], - "usage": {}, - "hidden_params": {}, - } + # if files is None or len(files) == 0: + # # --- BEGIN MOCK VIDEO RESPONSE --- + # mock_video_response = { + # "data": [ + # { + # "id": "video_123", + # "object": "video", + # "model": "sora-2", + # "status": "queued", + # "progress": 0, + # "created_at": 1712697600, + # "size": "1024x1808", + # "seconds": "8", + # "quality": "standard", + # } + # ], + # "usage": {}, + # "hidden_params": {}, + # } + + # import types + # class MockHTTPXResponse: + # def __init__(self, json_data): + # self._json_data = json_data + # self.status_code = 200 + # self.text = str(json_data) + # def json(self): + # return self._json_data + # response = MockHTTPXResponse(mock_video_response) + if files: + # Use multipart/form-data when files are present + response = sync_httpx_client.post( + url=api_base, + headers=headers, + data=data, + files=files, + timeout=timeout, + ) - import types - class MockHTTPXResponse: - def __init__(self, json_data): - self._json_data = json_data - self.status_code = 200 - self.text = str(json_data) - def json(self): - return self._json_data - response = MockHTTPXResponse(mock_video_response) # --- END MOCK VIDEO RESPONSE --- else: - print(f"DEBUG: Using multipart form data request") response = sync_httpx_client.post( url=api_base, headers=headers, @@ -4025,6 +4035,7 @@ async def async_video_generation_handler( def video_content_handler( self, video_id: str, + model: str, video_content_provider_config: "BaseVideoRetrievalConfig", custom_llm_provider: str, litellm_params: GenericLiteLLMParams, @@ -4033,11 +4044,25 @@ def video_content_handler( extra_headers: Optional[Dict[str, Any]] = None, api_key: Optional[str] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, - variant: Optional[str] = None, - ) -> bytes: + _is_async: bool = False, + ) -> Union[bytes, Coroutine[Any, Any, bytes]]: """ Handle video content download requests. """ + if _is_async: + return self.async_video_content_handler( + video_id=video_id, + model=model, + video_content_provider_config=video_content_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + timeout=timeout, + extra_headers=extra_headers, + api_key=api_key, + client=client, + ) + if client is None or not isinstance(client, HTTPHandler): sync_httpx_client = _get_httpx_client( params={"ssl_verify": litellm_params.get("ssl_verify", None)} @@ -4046,16 +4071,16 @@ def video_content_handler( sync_httpx_client = client headers = video_content_provider_config.validate_environment( - api_key=api_key, headers=extra_headers or {}, - model="", # No model needed for content download + model=model, + api_key=api_key, ) if extra_headers: headers.update(extra_headers) api_base = video_content_provider_config.get_complete_url( - model="", # No model needed for content download + model=model, api_base=litellm_params.get("api_base", None), litellm_params=dict(litellm_params), ) @@ -4064,10 +4089,8 @@ def video_content_handler( url = f"{api_base.rstrip('/')}/{video_id}/content" # Add variant query parameter if provided - params = {} - if variant: - params["variant"] = variant - + params = { "video_id": video_id } + try: # Make the GET request to download content response = sync_httpx_client.get( @@ -4076,8 +4099,12 @@ def video_content_handler( params=params, ) - # Return the raw content as bytes - return response.content + # Transform the response using the provider config + return video_content_provider_config.transform_video_retrieve_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) except Exception as e: raise self._handle_error( @@ -4088,6 +4115,7 @@ def video_content_handler( async def async_video_content_handler( self, video_id: str, + model: str, video_content_provider_config: "BaseVideoRetrievalConfig", custom_llm_provider: str, litellm_params: GenericLiteLLMParams, @@ -4096,7 +4124,6 @@ async def async_video_content_handler( extra_headers: Optional[Dict[str, Any]] = None, api_key: Optional[str] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, - variant: Optional[str] = None, ) -> bytes: """ Async version of the video content download handler. @@ -4110,16 +4137,16 @@ async def async_video_content_handler( async_httpx_client = client headers = video_content_provider_config.validate_environment( - api_key=api_key, headers=extra_headers or {}, - model="", # No model needed for content download + model=model, + api_key=api_key, ) if extra_headers: headers.update(extra_headers) api_base = video_content_provider_config.get_complete_url( - model="", # No model needed for content download + model=model, api_base=litellm_params.get("api_base", None), litellm_params=dict(litellm_params), ) @@ -4127,10 +4154,9 @@ async def async_video_content_handler( # Construct the URL for video content download url = f"{api_base.rstrip('/')}/{video_id}/content" - # Add variant query parameter if provided - params = {} - if variant: - params["variant"] = variant + params = { + "video_id": video_id, + } try: # Make the GET request to download content @@ -4140,8 +4166,12 @@ async def async_video_content_handler( params=params, ) - # Return the raw content as bytes - return response.content + # Transform the response using the provider config + return video_content_provider_config.transform_video_retrieve_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) except Exception as e: raise self._handle_error( diff --git a/litellm/llms/openai/video_generation/transformation.py b/litellm/llms/openai/video_generation/transformation.py index a610f608125d..40159d1e2e9a 100644 --- a/litellm/llms/openai/video_generation/transformation.py +++ b/litellm/llms/openai/video_generation/transformation.py @@ -1,14 +1,15 @@ -import types -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, IO from io import BufferedReader import httpx from httpx._types import RequestFiles from litellm.types.videos.main import VideoCreateOptionalRequestParams -from litellm.types.llms.openai import CreateVideoRequest, OpenAIVideoObject +from litellm.types.llms.openai import CreateVideoRequest from litellm.types.videos.main import VideoResponse from litellm.types.router import GenericLiteLLMParams +from litellm.secret_managers.main import get_secret_str +import litellm if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -25,7 +26,6 @@ LiteLLMLoggingObj = Any BaseVideoGenerationConfig = Any BaseLLMException = Any - VideoResponse = Any class OpenAIVideoGenerationConfig(BaseVideoGenerationConfig): @@ -65,15 +65,17 @@ def validate_environment( model: str, api_key: Optional[str] = None, ) -> dict: - """ - Validate the environment for OpenAI video generation. - """ - if api_key is None: - raise ValueError("OpenAI API key is required for video generation") - - headers["Authorization"] = f"Bearer {api_key}" - headers["Content-Type"] = "application/json" - + api_key = ( + api_key + or litellm.api_key + or litellm.openai_key + or get_secret_str("OPENAI_API_KEY") + ) + headers.update( + { + "Authorization": f"Bearer {api_key}", + } + ) return headers def get_complete_url( @@ -115,7 +117,7 @@ def transform_video_create_request( ) # Handle file uploads - files_list: RequestFiles = [] + files_list: List[Tuple[str, Tuple[str, Union[IO[bytes], bytes, str], str]]] = [] # Handle input_reference parameter if provided _input_reference = video_create_optional_request_params.get("input_reference") @@ -140,7 +142,7 @@ def transform_video_create_request( ) # Convert to dict for JSON serialization - data = video_create_request.model_dump(exclude_none=True) + data = dict(video_create_request) return data, files_list @@ -156,9 +158,10 @@ def transform_video_create_response( response_data = raw_response.json() # Transform the response data + from litellm.types.videos.main import VideoObject video_objects = [] for video_data in response_data.get("data", []): - video_obj = OpenAIVideoObject(**video_data) + video_obj = VideoObject(**video_data) video_objects.append(video_obj) # Create the response diff --git a/litellm/llms/openai/video_retrieval/transformation.py b/litellm/llms/openai/video_retrieval/transformation.py index 80fc95544d6b..a170fe3e1169 100644 --- a/litellm/llms/openai/video_retrieval/transformation.py +++ b/litellm/llms/openai/video_retrieval/transformation.py @@ -1,11 +1,9 @@ -import types -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union import httpx -from litellm.types.llms.openai import OpenAIVideoObject -from litellm.types.videos.main import VideoResponse -from litellm.types.router import GenericLiteLLMParams +from litellm.secret_managers.main import get_secret_str +import litellm if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -39,16 +37,19 @@ def validate_environment( model: str, api_key: Optional[str] = None, ) -> dict: - """ - Validate the environment for OpenAI video retrieval. - """ - if api_key is None: - raise ValueError("OpenAI API key is required for video retrieval") - - headers["Authorization"] = f"Bearer {api_key}" - headers["Content-Type"] = "application/json" - + api_key = ( + api_key + or litellm.api_key + or litellm.openai_key + or get_secret_str("OPENAI_API_KEY") + ) + headers.update( + { + "Authorization": f"Bearer {api_key}", + } + ) return headers + def get_complete_url( self, @@ -57,7 +58,8 @@ def get_complete_url( litellm_params: dict, ) -> str: """ - Get the complete URL for OpenAI video retrieval. + Get the complete URL for OpenAI video operations. + For video content download, this returns the base videos URL. """ if api_base is None: api_base = "https://api.openai.com/v1" @@ -69,23 +71,13 @@ def transform_video_retrieve_response( model: str, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, - ) -> VideoResponse: + ) -> bytes: """ - Transform the OpenAI video retrieval response. + Transform the OpenAI video content download response. + Returns raw video content as bytes. """ - response_data = raw_response.json() - - # Transform the response data - video_obj = OpenAIVideoObject(**response_data) - - # Create the response - response = VideoResponse( - data=[video_obj], - usage={}, - hidden_params={}, - ) - - return response + # For video content download, return the raw content as bytes + return raw_response.content def get_error_class( self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 31fe74df5cd1..7365f00e2d27 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -2,40 +2,15 @@ from os import PathLike from typing import IO, Any, Iterable, List, Literal, Mapping, Optional, Tuple, Union -import httpx from openai._legacy_response import ( HttpxBinaryResponseContent as _HttpxBinaryResponseContent, ) -from openai.lib.streaming._assistants import ( - AssistantEventHandler, - AssistantStreamManager, - AsyncAssistantEventHandler, - AsyncAssistantStreamManager, -) -from openai.pagination import AsyncCursorPage, SyncCursorPage -from openai.types import Batch, EmbeddingCreateParams, FileObject -from openai.types.beta.assistant import Assistant -from openai.types.beta.assistant_tool_param import AssistantToolParam -from openai.types.beta.thread_create_params import ( - Message as OpenAICreateThreadParamsMessage, -) -from openai.types.beta.threads.message import Message as OpenAIMessage -from openai.types.beta.threads.message_content import MessageContent -from openai.types.beta.threads.run import Run from openai.types.chat import ChatCompletionChunk -from openai.types.chat.chat_completion_audio_param import ChatCompletionAudioParam from openai.types.chat.chat_completion_content_part_input_audio_param import ( ChatCompletionContentPartInputAudioParam, ) -from openai.types.chat.chat_completion_modality import ChatCompletionModality -from openai.types.chat.chat_completion_prediction_content_param import ( - ChatCompletionPredictionContentParam, -) -from openai.types.embedding import Embedding as OpenAIEmbedding -from openai.types.fine_tuning.fine_tuning_job import FineTuningJob from openai.types.responses.response import ( IncompleteDetails, - Response, ResponseOutputItem, Tool, ToolChoice, @@ -54,11 +29,10 @@ Reasoning, ResponseIncludable, ResponseInputParam, - ToolChoice, ToolParam, ) from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall -from pydantic import BaseModel, ConfigDict, Discriminator, Field, PrivateAttr +from pydantic import BaseModel, ConfigDict, Discriminator, PrivateAttr from typing_extensions import Annotated, Dict, Required, TypedDict, override from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject diff --git a/litellm/types/utils.py b/litellm/types/utils.py index f1eeee4cbbd5..c6435131b241 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -9,29 +9,18 @@ Literal, Mapping, Optional, - Tuple, Union, ) -from aiohttp import FormData from openai._models import BaseModel as OpenAIObject -from openai.types.audio.transcription_create_params import FileTypes # type: ignore -from openai.types.chat.chat_completion import ChatCompletion from openai.types.completion_usage import ( CompletionTokensDetails, CompletionUsage, PromptTokensDetails, ) -from openai.types.moderation import ( - Categories, - CategoryAppliedInputTypes, - CategoryScores, -) -from openai.types.moderation_create_response import Moderation, ModerationCreateResponse from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator -from typing_extensions import Callable, Dict, Required, TypedDict, override +from typing_extensions import Required, TypedDict -import litellm from litellm._uuid import uuid from litellm.types.llms.base import ( BaseLiteLLMOpenAIResponseObject, @@ -57,7 +46,6 @@ OpenAIRealtimeStreamList, WebSearchOptions, ) -from .rerank import RerankResponse if TYPE_CHECKING: from .vector_stores import VectorStoreSearchResponse @@ -1167,7 +1155,6 @@ def __init__(self, **kwargs): super().__init__(**kwargs) -from openai.types.chat import ChatCompletionChunk class ModelResponseBase(OpenAIObject): diff --git a/litellm/types/videos/main.py b/litellm/types/videos/main.py index 30d0b4912db5..283c6ca7ad94 100644 --- a/litellm/types/videos/main.py +++ b/litellm/types/videos/main.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Literal, Optional from typing_extensions import TypedDict from pydantic import BaseModel diff --git a/litellm/utils.py b/litellm/utils.py index ce0c00bf3652..1b15c98fa1d9 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -263,6 +263,7 @@ BaseImageVariationConfig, ) from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig +from litellm.llms.base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig @@ -7565,6 +7566,19 @@ def get_provider_video_generation_config( return OpenAIVideoGenerationConfig() return None + @staticmethod + def get_provider_video_content_config( + model: str, + provider: LlmProviders, + ) -> Optional[BaseVideoRetrievalConfig]: + if LlmProviders.OPENAI == provider: + from litellm.llms.openai.video_retrieval.transformation import ( + OpenAIVideoRetrievalConfig, + ) + + return OpenAIVideoRetrievalConfig() + return None + @staticmethod def get_provider_realtime_config( model: str, diff --git a/litellm/videos/main.py b/litellm/videos/main.py index 5aa65d4164f2..eb78c64ef2dc 100644 --- a/litellm/videos/main.py +++ b/litellm/videos/main.py @@ -1,12 +1,10 @@ import asyncio import contextvars -import time from functools import partial from typing import Any, Coroutine, Literal, Optional, Union, overload, Dict import litellm import orjson -from litellm._logging import verbose_logger from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.types.videos.main import ( VideoCreateOptionalRequestParams, @@ -17,86 +15,98 @@ from litellm.videos.utils import VideoGenerationRequestUtils from litellm.constants import DEFAULT_VIDEO_ENDPOINT_MODEL, request_timeout as DEFAULT_REQUEST_TIMEOUT from litellm.main import base_llm_http_handler -from litellm.types.utils import all_litellm_params, LlmProviders, CallTypes -from litellm.utils import client, exception_type, ProviderConfigManager +from litellm.utils import client, ProviderConfigManager from litellm.types.utils import FileTypes from litellm.types.router import GenericLiteLLMParams from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig -from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler +from litellm.llms.base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler -from litellm.llms.custom_llm import CustomLLM #################### Initialize provider clients #################### llm_http_handler: BaseLLMHTTPHandler = BaseLLMHTTPHandler() ##### Video Generation ####################### @client -async def avideo_generation(*args, **kwargs) -> VideoResponse: +async def avideo_generation( + prompt: str, + model: Optional[str] = None, + input_reference: Optional[FileTypes] = None, + seconds: Optional[str] = None, + size: Optional[str] = None, + user: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> VideoResponse: """ Asynchronously calls the `video_generation` function with the given arguments and keyword arguments. Parameters: - - `args` (tuple): Positional arguments to be passed to the `video_generation` function. - - `kwargs` (dict): Keyword arguments to be passed to the `video_generation` function. + - `prompt` (str): Text prompt that describes the video to generate + - `model` (Optional[str]): The video generation model to use + - `input_reference` (Optional[FileTypes]): Optional image reference that guides generation + - `seconds` (Optional[str]): Clip duration in seconds + - `size` (Optional[str]): Output resolution formatted as width x height + - `user` (Optional[str]): A unique identifier representing your end-user + - `timeout` (int): Request timeout in seconds + - `custom_llm_provider` (Optional[str]): The LLM provider to use + - `extra_headers` (Optional[Dict[str, Any]]): Additional headers + - `extra_query` (Optional[Dict[str, Any]]): Additional query parameters + - `extra_body` (Optional[Dict[str, Any]]): Additional body parameters + - `kwargs` (dict): Additional keyword arguments Returns: - `response` (VideoResponse): The response returned by the `video_generation` function. """ - loop = asyncio.get_event_loop() - model = args[0] if len(args) > 0 else kwargs["model"] - ### PASS ARGS TO Video Generation ### - kwargs["avideo_generation"] = True - custom_llm_provider = None + local_vars = locals() try: - # Check for mock response first - mock_response = kwargs.get("mock_response", None) - if mock_response is not None: - if isinstance(mock_response, str): - mock_response = orjson.loads(mock_response) - - response = VideoResponse( - data=[VideoObject(**video_data) for video_data in mock_response.get("data", [])], - usage=VideoUsage(**mock_response.get("usage", {})), - hidden_params=kwargs.get("hidden_params", {}), + loop = asyncio.get_event_loop() + kwargs["async_call"] = True + + # get custom llm provider so we can use this for mapping exceptions + if custom_llm_provider is None: + _, custom_llm_provider, _, _ = litellm.get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, api_base=local_vars.get("api_base", None) ) - return response - # Use a partial function to pass your keyword arguments - func = partial(video_generation, *args, **kwargs) + func = partial( + video_generation, + prompt=prompt, + model=model, + input_reference=input_reference, + seconds=seconds, + size=size, + user=user, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + **kwargs, + ) - # Add the context to the function ctx = contextvars.copy_context() func_with_context = partial(ctx.run, func) - - _, custom_llm_provider, _, _ = get_llm_provider( - model=model, api_base=kwargs.get("api_base", None) - ) - - # Await normally init_response = await loop.run_in_executor(None, func_with_context) - response: Optional[VideoResponse] = None - if isinstance(init_response, dict): - response = VideoResponse(**init_response) - elif isinstance(init_response, VideoResponse): ## CACHING SCENARIO + if asyncio.iscoroutine(init_response): + response = await init_response + else: response = init_response - elif asyncio.iscoroutine(init_response): - response = await init_response # type: ignore - - if response is None: - raise ValueError( - "Unable to get Video Response. Please pass a valid llm_provider." - ) return response except Exception as e: - custom_llm_provider = custom_llm_provider or "openai" - raise exception_type( + raise litellm.exception_type( model=model, custom_llm_provider=custom_llm_provider, original_exception=e, - completion_kwargs=args, + completion_kwargs=local_vars, extra_kwargs=kwargs, ) @@ -208,7 +218,7 @@ def video_generation( # noqa: PLR0915 raise ValueError(f"image edit is not supported for {custom_llm_provider}") local_vars.update(kwargs) - # Get ImageEditOptionalRequestParams with only valid parameters + # Get VideoGenerationOptionalRequestParams with only valid parameters video_generation_optional_params: VideoCreateOptionalRequestParams = ( VideoGenerationRequestUtils.get_requested_video_generation_optional_param(local_vars) ) @@ -260,508 +270,197 @@ def video_generation( # noqa: PLR0915 ) -##### Video Retrieval ####################### -@client -async def avideo_retrieve(*args, **kwargs) -> VideoResponse: - """ - Asynchronously retrieve a video generation job status. - """ - loop = asyncio.get_event_loop() - video_id = args[0] if len(args) > 0 else kwargs["video_id"] - - kwargs["avideo_retrieve"] = True - custom_llm_provider = None - try: - # Check for mock response first - mock_response = kwargs.get("mock_response", None) - if mock_response is not None: - if isinstance(mock_response, str): - mock_response = orjson.loads(mock_response) - - response = VideoResponse( - data=[VideoObject(**video_data) for video_data in mock_response.get("data", [])], - usage=VideoUsage(**mock_response.get("usage", {})), - hidden_params=kwargs.get("hidden_params", {}), - ) - return response - - # Use a partial function to pass your keyword arguments - func = partial(video_retrieve, *args, **kwargs) - - # Add the context to the function - ctx = contextvars.copy_context() - func_with_context = partial(ctx.run, func) - - _, custom_llm_provider, _, _ = get_llm_provider( - model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) - ) - - # Await normally - init_response = await loop.run_in_executor(None, func_with_context) - - response: Optional[VideoResponse] = None - if isinstance(init_response, dict): - response = VideoResponse(**init_response) - elif isinstance(init_response, VideoResponse): ## CACHING SCENARIO - response = init_response - elif asyncio.iscoroutine(init_response): - response = await init_response # type: ignore - - if response is None: - raise ValueError( - "Unable to get Video Response. Please pass a valid llm_provider." - ) - - return response - except Exception as e: - custom_llm_provider = custom_llm_provider or "openai" - raise exception_type( - model=kwargs.get("model", "sora-2"), - custom_llm_provider=custom_llm_provider, - original_exception=e, - completion_kwargs=args, - extra_kwargs=kwargs, - ) - - @client -def video_retrieve( +def video_content( video_id: str, model: Optional[str] = None, - api_key: Optional[str] = None, api_base: Optional[str] = None, timeout: Optional[float] = None, custom_llm_provider: Optional[str] = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, **kwargs, -) -> VideoResponse: - """ - Retrieve a video generation job status. +) -> Union[ + bytes, + Coroutine[Any, Any, bytes], +]: """ - try: - if custom_llm_provider is None: - custom_llm_provider = "openai" + Download video content from OpenAI's video API. - # For now, return a placeholder response since video retrieval is not yet implemented - verbose_logger.warning( - f"Video retrieval for provider {custom_llm_provider} is not yet implemented." - ) - - model_response = VideoResponse( - data=[ - VideoObject( - id=video_id, - object="video", - status="completed", - created_at=int(time.time()), - completed_at=int(time.time()), - model=model or "sora-2" - ) - ], - usage=VideoUsage(), - hidden_params=kwargs.get("hidden_params", {}), - ) + Args: + video_id (str): The identifier of the video whose content to download. + model (Optional[str]): The model to use. If not provided, will be auto-detected. + api_key (Optional[str]): The API key to use for authentication. + api_base (Optional[str]): The base URL for the API. + timeout (Optional[float]): The timeout for the request in seconds. + custom_llm_provider (Optional[str]): The LLM provider to use. If not provided, will be auto-detected. + variant (Optional[str]): Which downloadable asset to return. Defaults to the MP4 video. + extra_headers (Optional[Dict[str, Any]]): Additional headers to include in the request. + extra_query (Optional[Dict[str, Any]]): Additional query parameters. + extra_body (Optional[Dict[str, Any]]): Additional body parameters. - return model_response + Returns: + bytes: The raw video content as bytes. - except Exception as e: - verbose_logger.error(f"Error in video_retrieve: {e}") - raise e + Example: + ```python + import litellm + # Download video content + video_bytes = litellm.video_content( + video_id="video_123", + custom_llm_provider="openai" + ) -##### Video Deletion ####################### -@client -async def avideo_delete(*args, **kwargs) -> bool: - """ - Asynchronously delete a video generation job. + # Save to file + with open("video.mp4", "wb") as f: + f.write(video_bytes) + ``` """ - loop = asyncio.get_event_loop() - video_id = args[0] if len(args) > 0 else kwargs["video_id"] - - kwargs["avideo_delete"] = True - custom_llm_provider = None + local_vars = locals() try: - # Use a partial function to pass your keyword arguments - func = partial(video_delete, *args, **kwargs) - - # Add the context to the function - ctx = contextvars.copy_context() - func_with_context = partial(ctx.run, func) - - _, custom_llm_provider, _, _ = get_llm_provider( - model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) - ) + litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore + litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None) + _is_async = kwargs.pop("async_call", False) is True - # Await normally - result = await loop.run_in_executor(None, func_with_context) - return result - except Exception as e: - custom_llm_provider = custom_llm_provider or "openai" - raise exception_type( - model=kwargs.get("model", "sora-2"), + # get llm provider logic + litellm_params = GenericLiteLLMParams(**kwargs) + model, custom_llm_provider, _, _ = get_llm_provider( + model=model or "sora-2", # Default model for video content custom_llm_provider=custom_llm_provider, - original_exception=e, - completion_kwargs=args, - extra_kwargs=kwargs, ) - -@client -def video_delete( - video_id: str, - model: Optional[str] = None, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - timeout: Optional[float] = None, - custom_llm_provider: Optional[str] = None, - **kwargs, -) -> bool: - """ - Delete a video generation job. - """ - try: - if custom_llm_provider is None: - custom_llm_provider = "openai" - - # For now, return a placeholder response since video deletion is not yet implemented - verbose_logger.warning( - f"Video deletion for provider {custom_llm_provider} is not yet implemented." + # get provider config + video_content_provider_config: Optional[BaseVideoRetrievalConfig] = ( + ProviderConfigManager.get_provider_video_content_config( + model=model, + provider=litellm.LlmProviders(custom_llm_provider), + ) ) - result = True - return result + if video_content_provider_config is None: + raise ValueError(f"video content download is not supported for {custom_llm_provider}") - except Exception as e: - verbose_logger.error(f"Error in video_delete: {e}") - raise e - - -##### Video Content Download ####################### -@client -async def avideo_content(*args, **kwargs) -> bytes: - """ - Asynchronously download video content. - """ - loop = asyncio.get_event_loop() - video_id = args[0] if len(args) > 0 else kwargs["video_id"] - - kwargs["avideo_content"] = True - custom_llm_provider = None - try: - # Use a partial function to pass your keyword arguments - func = partial(video_content, *args, **kwargs) + local_vars.update(kwargs) + # For video content download, we don't need complex optional parameter handling + # Just pass the basic parameters that are relevant for content download + video_content_request_params: Dict = { + "video_id": video_id, + } - # Add the context to the function - ctx = contextvars.copy_context() - func_with_context = partial(ctx.run, func) + # Pre Call logging + litellm_logging_obj.update_environment_variables( + model=model, + user=kwargs.get("user"), + optional_params=dict(video_content_request_params), + litellm_params={ + "litellm_call_id": litellm_call_id, + **video_content_request_params, + }, + custom_llm_provider=custom_llm_provider, + ) - _, custom_llm_provider, _, _ = get_llm_provider( - model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) + # Call the handler with _is_async flag instead of directly calling the async handler + return base_llm_http_handler.video_content_handler( + video_id=video_id, + model=model, + video_content_provider_config=video_content_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=litellm_logging_obj, + timeout=timeout or DEFAULT_REQUEST_TIMEOUT, + extra_headers=extra_headers, + client=kwargs.get("client"), + _is_async=_is_async, ) - # Await normally - result = await loop.run_in_executor(None, func_with_context) - return result except Exception as e: - custom_llm_provider = custom_llm_provider or "openai" - raise exception_type( - model=kwargs.get("model", "sora-2"), + raise litellm.exception_type( + model=model, custom_llm_provider=custom_llm_provider, original_exception=e, - completion_kwargs=args, + completion_kwargs=local_vars, extra_kwargs=kwargs, ) +##### Video Content Download ####################### @client -def video_content( +async def avideo_content( video_id: str, model: Optional[str] = None, api_key: Optional[str] = None, api_base: Optional[str] = None, timeout: Optional[float] = None, custom_llm_provider: Optional[str] = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, **kwargs, ) -> bytes: """ - Download video content. - """ - try: - if custom_llm_provider is None: - custom_llm_provider = "openai" - - # For now, return placeholder content - verbose_logger.warning( - f"Video content download for provider {custom_llm_provider} is not yet implemented." - ) - - # Return placeholder video content - return b"placeholder video content" - - except Exception as e: - verbose_logger.error(f"Error in video_content: {e}") - raise e + Asynchronously download video content. + Parameters: + - `video_id` (str): The identifier of the video whose content to download + - `model` (Optional[str]): The model to use. If not provided, will be auto-detected + - `api_key` (Optional[str]): The API key to use for authentication + - `api_base` (Optional[str]): The base URL for the API + - `timeout` (Optional[float]): The timeout for the request in seconds + - `custom_llm_provider` (Optional[str]): The LLM provider to use + - `extra_headers` (Optional[Dict[str, Any]]): Additional headers + - `extra_query` (Optional[Dict[str, Any]]): Additional query parameters + - `extra_body` (Optional[Dict[str, Any]]): Additional body parameters + - `kwargs` (dict): Additional keyword arguments -##### Video Listing ####################### -@client -async def avideo_list(*args, **kwargs) -> VideoResponse: - """ - Asynchronously list video generation jobs. + Returns: + - `bytes`: The raw video content as bytes """ - loop = asyncio.get_event_loop() - - kwargs["avideo_list"] = True - custom_llm_provider = None + local_vars = locals() try: - # Use a partial function to pass your keyword arguments - func = partial(video_list, *args, **kwargs) + loop = asyncio.get_event_loop() + kwargs["async_call"] = True - # Add the context to the function - ctx = contextvars.copy_context() - func_with_context = partial(ctx.run, func) + # get custom llm provider so we can use this for mapping exceptions + if custom_llm_provider is None: + _, custom_llm_provider, _, _ = litellm.get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, api_base=api_base + ) - _, custom_llm_provider, _, _ = get_llm_provider( - model=kwargs.get("model", "sora-2"), api_base=kwargs.get("api_base", None) + func = partial( + video_content, + video_id=video_id, + model=model, + api_key=api_key, + api_base=api_base, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + **kwargs, ) - # Await normally + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) init_response = await loop.run_in_executor(None, func_with_context) - response: Optional[VideoResponse] = None - if isinstance(init_response, dict): - response = VideoResponse(**init_response) - elif isinstance(init_response, VideoResponse): ## CACHING SCENARIO + if asyncio.iscoroutine(init_response): + response = await init_response + else: response = init_response - elif asyncio.iscoroutine(init_response): - response = await init_response # type: ignore - - if response is None: - raise ValueError( - "Unable to get Video Response. Please pass a valid llm_provider." - ) return response except Exception as e: - custom_llm_provider = custom_llm_provider or "openai" - raise exception_type( - model=kwargs.get("model", "sora-2"), + raise litellm.exception_type( + model=model, custom_llm_provider=custom_llm_provider, original_exception=e, - completion_kwargs=args, + completion_kwargs=local_vars, extra_kwargs=kwargs, ) - - -@client -def video_list( - model: Optional[str] = None, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - timeout: Optional[float] = None, - custom_llm_provider: Optional[str] = None, - **kwargs, -) -> VideoResponse: - """ - List video generation jobs. - """ - try: - if custom_llm_provider is None: - custom_llm_provider = "openai" - - # For now, return placeholder response - verbose_logger.warning( - f"Video listing for provider {custom_llm_provider} is not yet implemented." - ) - - model_response = VideoResponse( - data=[], - usage=VideoUsage(), - hidden_params=kwargs.get("hidden_params", {}), - ) - - return model_response - - except Exception as e: - verbose_logger.error(f"Error in video_list: {e}") - raise e - - -# Convenience functions with better names -create_video = video_generation -acreate_video = avideo_generation - - -def video_content( - video_id: str, - custom_llm_provider: Optional[str] = None, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - timeout: Optional[float] = None, - extra_headers: Optional[Dict[str, Any]] = None, - litellm_params: Optional[Dict[str, Any]] = None, - client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, - variant: Optional[str] = None, -) -> bytes: - """ - Download video content from OpenAI's video API. - - Args: - video_id (str): The identifier of the video whose content to download. - custom_llm_provider (Optional[str]): The LLM provider to use. If not provided, will be auto-detected. - api_key (Optional[str]): The API key to use for authentication. - api_base (Optional[str]): The base URL for the API. - timeout (Optional[float]): The timeout for the request in seconds. - extra_headers (Optional[Dict[str, Any]]): Additional headers to include in the request. - litellm_params (Optional[Dict[str, Any]]): Additional LiteLLM parameters. - client (Optional[Union[HTTPHandler, AsyncHTTPHandler]]): The HTTP client to use. - variant (Optional[str]): Which downloadable asset to return. Defaults to the MP4 video. - - Returns: - bytes: The raw video content as bytes. - - Example: - ```python - import litellm - - # Download video content - video_bytes = litellm.video_content( - video_id="video_123", - custom_llm_provider="openai" - ) - - # Save to file - with open("video.mp4", "wb") as f: - f.write(video_bytes) - ``` - """ - if litellm_params is None: - litellm_params = {} - - # Set default timeout if not provided - if timeout is None: - timeout = DEFAULT_REQUEST_TIMEOUT - - # Get the LLM provider if not provided - if custom_llm_provider is None: - custom_llm_provider = "openai" # Default to OpenAI for video content - - # Get the provider config - from litellm.llms.openai.video_retrieval.transformation import OpenAIVideoRetrievalConfig - video_content_provider_config = OpenAIVideoRetrievalConfig() - - # Create logging object - logging_obj = LiteLLMLoggingObj( - model="", # No model needed for content download - messages=[], # No messages for content download - stream=False, - call_type=CallTypes.video_content.value, - start_time=time.time(), - litellm_call_id="", - function_id="video_content", - ) - - # Get the HTTP handler - from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler - base_llm_http_handler = BaseLLMHTTPHandler() - - # Call the video content handler - return base_llm_http_handler.video_content_handler( - video_id=video_id, - video_content_provider_config=video_content_provider_config, - custom_llm_provider=custom_llm_provider, - litellm_params=litellm_params, - logging_obj=logging_obj, - timeout=timeout, - extra_headers=extra_headers, - api_key=api_key, - client=client, - variant=variant, - ) - - -async def async_video_content( - video_id: str, - custom_llm_provider: Optional[str] = None, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - timeout: Optional[float] = None, - extra_headers: Optional[Dict[str, Any]] = None, - litellm_params: Optional[Dict[str, Any]] = None, - client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, - variant: Optional[str] = None, -) -> bytes: - """ - Async version of video content download. - - Args: - video_id (str): The identifier of the video whose content to download. - custom_llm_provider (Optional[str]): The LLM provider to use. If not provided, will be auto-detected. - api_key (Optional[str]): The API key to use for authentication. - api_base (Optional[str]): The base URL for the API. - timeout (Optional[float]): The timeout for the request in seconds. - extra_headers (Optional[Dict[str, Any]]): Additional headers to include in the request. - litellm_params (Optional[Dict[str, Any]]): Additional LiteLLM parameters. - client (Optional[Union[HTTPHandler, AsyncHTTPHandler]]): The HTTP client to use. - variant (Optional[str]): Which downloadable asset to return. Defaults to the MP4 video. - - Returns: - bytes: The raw video content as bytes. - - Example: - ```python - import litellm - - # Download video content asynchronously - video_bytes = await litellm.async_video_content( - video_id="video_123", - custom_llm_provider="openai" - ) - - # Save to file - with open("video.mp4", "wb") as f: - f.write(video_bytes) - ``` - """ - if litellm_params is None: - litellm_params = {} - - # Set default timeout if not provided - if timeout is None: - timeout = DEFAULT_REQUEST_TIMEOUT - - # Get the LLM provider if not provided - if custom_llm_provider is None: - custom_llm_provider = "openai" # Default to OpenAI for video content - - # Get the provider config - from litellm.llms.openai.video_retrieval.transformation import OpenAIVideoRetrievalConfig - video_content_provider_config = OpenAIVideoRetrievalConfig() - - # Create logging object - logging_obj = LiteLLMLoggingObj( - model="", # No model needed for content download - messages=[], # No messages for content download - stream=False, - call_type=CallTypes.video_content.value, - start_time=time.time(), - litellm_call_id="", - function_id="async_video_content", - ) - - # Get the HTTP handler - from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler - base_llm_http_handler = BaseLLMHTTPHandler() - - # Call the async video content handler - return await base_llm_http_handler.async_video_content_handler( - video_id=video_id, - video_content_provider_config=video_content_provider_config, - custom_llm_provider=custom_llm_provider, - litellm_params=litellm_params, - logging_obj=logging_obj, - timeout=timeout, - extra_headers=extra_headers, - api_key=api_key, - client=client, - variant=variant, - ) \ No newline at end of file From e5ea94267a369868b0c96c31c07b998956d34824 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 14 Oct 2025 22:46:20 +0530 Subject: [PATCH 03/17] Add docs --- docs/my-website/docs/providers/openai.md | 122 ++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/providers/openai.md b/docs/my-website/docs/providers/openai.md index 3fad78dc80e4..c1dfd3c3e9e2 100644 --- a/docs/my-website/docs/providers/openai.md +++ b/docs/my-website/docs/providers/openai.md @@ -836,4 +836,124 @@ response = completion( model="gpt-5-pro", messages=[{"role": "user", "content": "Solve this complex reasoning problem..."}] ) -``` \ No newline at end of file +``` + +## Video Generation + +LiteLLM supports OpenAI's video generation models including Sora. + +### Quick Start + +```python +from litellm import video_generation, video_content +import os + +os.environ["OPENAI_API_KEY"] = "your-api-key" + +# Generate a video +response = video_generation( + prompt="A cat playing with a ball of yarn in a sunny garden", + model="sora-2", + seconds="8", + size="720x1280" +) + +print(f"Video ID: {response.data[0].id}") +print(f"Status: {response.data[0].status}") + +# Download video content when ready +video_bytes = video_content( + video_id=response.data[0].id, + model="sora-2" +) + +# Save to file +with open("generated_video.mp4", "wb") as f: + f.write(video_bytes) +``` + +### Supported Models + +| Model Name | Description | Max Duration | Supported Sizes | +|------------|-------------|--------------|-----------------| +| sora-2 | OpenAI's latest video generation model | 8 seconds | 720x1280, 1280x720 | + +### Video Generation Parameters + +- `prompt` (required): Text description of the desired video +- `model` (optional): Model to use, defaults to "sora-2" +- `seconds` (optional): Video duration in seconds (e.g., "8", "16") +- `size` (optional): Video dimensions (e.g., "720x1280", "1280x720") +- `input_reference` (optional): Reference image for video editing +- `user` (optional): User identifier for tracking + +### Video Content Retrieval + +```python +# Download video content +video_bytes = video_content( + video_id="video_1234567890", + model="sora-2" +) + +# Save to file +with open("video.mp4", "wb") as f: + f.write(video_bytes) +``` + +### Complete Workflow + +```python +import litellm +import time + +def generate_and_download_video(prompt): + # Step 1: Generate video + response = litellm.video_generation( + prompt=prompt, + model="sora-2", + seconds="8", + size="720x1280" + ) + + video_id = response.data[0].id + print(f"Video ID: {video_id}") + + # Step 2: Wait for processing (in practice, poll status) + time.sleep(30) + + # Step 3: Download video + video_bytes = litellm.video_content( + video_id=video_id, + model="sora-2" + ) + + # Step 4: Save to file + with open(f"video_{video_id}.mp4", "wb") as f: + f.write(video_bytes) + + return f"video_{video_id}.mp4" + +# Usage +video_file = generate_and_download_video( + "A cat playing with a ball of yarn in a sunny garden" +) +``` + +### Error Handling + +```python +from litellm.exceptions import BadRequestError, AuthenticationError + +try: + response = video_generation( + prompt="A cat playing with a ball of yarn", + model="sora-2" + ) +except AuthenticationError as e: + print(f"Authentication failed: {e}") +except BadRequestError as e: + print(f"Bad request: {e}") +``` + +For more detailed documentation, see [Video Generation →](../video_generation.md) \ No newline at end of file From 23facd6413a9445bff056ddfddad5b302a5c21fd Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 14 Oct 2025 22:56:32 +0530 Subject: [PATCH 04/17] Add imports --- litellm/types/llms/openai.py | 28 +++++++++++++++++++++++++++- litellm/types/utils.py | 18 +++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 7365f00e2d27..31fe74df5cd1 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -2,15 +2,40 @@ from os import PathLike from typing import IO, Any, Iterable, List, Literal, Mapping, Optional, Tuple, Union +import httpx from openai._legacy_response import ( HttpxBinaryResponseContent as _HttpxBinaryResponseContent, ) +from openai.lib.streaming._assistants import ( + AssistantEventHandler, + AssistantStreamManager, + AsyncAssistantEventHandler, + AsyncAssistantStreamManager, +) +from openai.pagination import AsyncCursorPage, SyncCursorPage +from openai.types import Batch, EmbeddingCreateParams, FileObject +from openai.types.beta.assistant import Assistant +from openai.types.beta.assistant_tool_param import AssistantToolParam +from openai.types.beta.thread_create_params import ( + Message as OpenAICreateThreadParamsMessage, +) +from openai.types.beta.threads.message import Message as OpenAIMessage +from openai.types.beta.threads.message_content import MessageContent +from openai.types.beta.threads.run import Run from openai.types.chat import ChatCompletionChunk +from openai.types.chat.chat_completion_audio_param import ChatCompletionAudioParam from openai.types.chat.chat_completion_content_part_input_audio_param import ( ChatCompletionContentPartInputAudioParam, ) +from openai.types.chat.chat_completion_modality import ChatCompletionModality +from openai.types.chat.chat_completion_prediction_content_param import ( + ChatCompletionPredictionContentParam, +) +from openai.types.embedding import Embedding as OpenAIEmbedding +from openai.types.fine_tuning.fine_tuning_job import FineTuningJob from openai.types.responses.response import ( IncompleteDetails, + Response, ResponseOutputItem, Tool, ToolChoice, @@ -29,10 +54,11 @@ Reasoning, ResponseIncludable, ResponseInputParam, + ToolChoice, ToolParam, ) from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall -from pydantic import BaseModel, ConfigDict, Discriminator, PrivateAttr +from pydantic import BaseModel, ConfigDict, Discriminator, Field, PrivateAttr from typing_extensions import Annotated, Dict, Required, TypedDict, override from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject diff --git a/litellm/types/utils.py b/litellm/types/utils.py index c6435131b241..6f787c8ce9b3 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -9,18 +9,29 @@ Literal, Mapping, Optional, + Tuple, Union, ) +from aiohttp import FormData from openai._models import BaseModel as OpenAIObject +from openai.types.audio.transcription_create_params import FileTypes # type: ignore +from openai.types.chat.chat_completion import ChatCompletion from openai.types.completion_usage import ( CompletionTokensDetails, CompletionUsage, PromptTokensDetails, ) +from openai.types.moderation import ( + Categories, + CategoryAppliedInputTypes, + CategoryScores, +) +from openai.types.moderation_create_response import Moderation, ModerationCreateResponse from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator -from typing_extensions import Required, TypedDict +from typing_extensions import Callable, Dict, Required, TypedDict, override +import litellm from litellm._uuid import uuid from litellm.types.llms.base import ( BaseLiteLLMOpenAIResponseObject, @@ -46,6 +57,7 @@ OpenAIRealtimeStreamList, WebSearchOptions, ) +from .rerank import RerankResponse if TYPE_CHECKING: from .vector_stores import VectorStoreSearchResponse @@ -272,10 +284,6 @@ class CallTypes(str, Enum): acreate_video = "acreate_video" avideo_retrieve = "avideo_retrieve" video_retrieve = "video_retrieve" - avideo_delete = "avideo_delete" - video_delete = "video_delete" - avideo_list = "avideo_list" - video_list = "video_list" avideo_content = "avideo_content" video_content = "video_content" acancel_fine_tuning_job = "acancel_fine_tuning_job" From 37ebb6bfc039d38078de71bff5d43889a4079e3e Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 14 Oct 2025 22:59:43 +0530 Subject: [PATCH 05/17] remove orjson --- litellm/videos/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/videos/main.py b/litellm/videos/main.py index eb78c64ef2dc..ba5f870d5b9f 100644 --- a/litellm/videos/main.py +++ b/litellm/videos/main.py @@ -3,8 +3,8 @@ from functools import partial from typing import Any, Coroutine, Literal, Optional, Union, overload, Dict +import json import litellm -import orjson from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.types.videos.main import ( VideoCreateOptionalRequestParams, @@ -190,7 +190,7 @@ def video_generation( # noqa: PLR0915 mock_response = kwargs.get("mock_response", None) if mock_response is not None: if isinstance(mock_response, str): - mock_response = orjson.loads(mock_response) + mock_response = json.loads(mock_response) response = VideoResponse( data=[VideoObject(**video_data) for video_data in mock_response.get("data", [])], From 2ae5fdbd71bd3ca0595dc93900c26e3aa50237f0 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 14 Oct 2025 23:06:32 +0530 Subject: [PATCH 06/17] remove double import --- litellm/llms/openai/video_generation/transformation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/llms/openai/video_generation/transformation.py b/litellm/llms/openai/video_generation/transformation.py index 40159d1e2e9a..55ff11159006 100644 --- a/litellm/llms/openai/video_generation/transformation.py +++ b/litellm/llms/openai/video_generation/transformation.py @@ -13,7 +13,6 @@ if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj - from litellm.types.videos.main import VideoResponse as _VideoResponse from ...base_llm.videos_generation.transformation import BaseVideoGenerationConfig as _BaseVideoGenerationConfig from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException @@ -21,7 +20,6 @@ LiteLLMLoggingObj = _LiteLLMLoggingObj BaseVideoGenerationConfig = _BaseVideoGenerationConfig BaseLLMException = _BaseLLMException - VideoResponse = _VideoResponse else: LiteLLMLoggingObj = Any BaseVideoGenerationConfig = Any From 76728dc1ff1c63c378f3fae2923e1f891e104a9d Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 15 Oct 2025 23:36:02 +0530 Subject: [PATCH 07/17] fix openai videos format --- docs/my-website/docs/providers/openai.md | 116 +---------- .../docs/providers/openai/video_generation.md | 143 ++++++++++++++ litellm/cost_calculator.py | 117 ++++++++++- .../video_retrieval/transformation.py | 89 --------- .../videos_generation/transformation.py | 119 ------------ litellm/llms/openai/cost_calculation.py | 44 +++++ .../openai/video_generation/transformation.py | 183 ------------------ .../openai/video_retrieval/transformation.py | 91 --------- litellm/types/videos/main.py | 25 +-- litellm/utils.py | 26 +-- model_prices_and_context_window.json | 16 ++ 11 files changed, 322 insertions(+), 647 deletions(-) create mode 100644 docs/my-website/docs/providers/openai/video_generation.md delete mode 100644 litellm/llms/base_llm/video_retrieval/transformation.py delete mode 100644 litellm/llms/base_llm/videos_generation/transformation.py delete mode 100644 litellm/llms/openai/video_generation/transformation.py delete mode 100644 litellm/llms/openai/video_retrieval/transformation.py diff --git a/docs/my-website/docs/providers/openai.md b/docs/my-website/docs/providers/openai.md index c1dfd3c3e9e2..a26b987dd1a9 100644 --- a/docs/my-website/docs/providers/openai.md +++ b/docs/my-website/docs/providers/openai.md @@ -842,118 +842,4 @@ response = completion( LiteLLM supports OpenAI's video generation models including Sora. -### Quick Start - -```python -from litellm import video_generation, video_content -import os - -os.environ["OPENAI_API_KEY"] = "your-api-key" - -# Generate a video -response = video_generation( - prompt="A cat playing with a ball of yarn in a sunny garden", - model="sora-2", - seconds="8", - size="720x1280" -) - -print(f"Video ID: {response.data[0].id}") -print(f"Status: {response.data[0].status}") - -# Download video content when ready -video_bytes = video_content( - video_id=response.data[0].id, - model="sora-2" -) - -# Save to file -with open("generated_video.mp4", "wb") as f: - f.write(video_bytes) -``` - -### Supported Models - -| Model Name | Description | Max Duration | Supported Sizes | -|------------|-------------|--------------|-----------------| -| sora-2 | OpenAI's latest video generation model | 8 seconds | 720x1280, 1280x720 | - -### Video Generation Parameters - -- `prompt` (required): Text description of the desired video -- `model` (optional): Model to use, defaults to "sora-2" -- `seconds` (optional): Video duration in seconds (e.g., "8", "16") -- `size` (optional): Video dimensions (e.g., "720x1280", "1280x720") -- `input_reference` (optional): Reference image for video editing -- `user` (optional): User identifier for tracking - -### Video Content Retrieval - -```python -# Download video content -video_bytes = video_content( - video_id="video_1234567890", - model="sora-2" -) - -# Save to file -with open("video.mp4", "wb") as f: - f.write(video_bytes) -``` - -### Complete Workflow - -```python -import litellm -import time - -def generate_and_download_video(prompt): - # Step 1: Generate video - response = litellm.video_generation( - prompt=prompt, - model="sora-2", - seconds="8", - size="720x1280" - ) - - video_id = response.data[0].id - print(f"Video ID: {video_id}") - - # Step 2: Wait for processing (in practice, poll status) - time.sleep(30) - - # Step 3: Download video - video_bytes = litellm.video_content( - video_id=video_id, - model="sora-2" - ) - - # Step 4: Save to file - with open(f"video_{video_id}.mp4", "wb") as f: - f.write(video_bytes) - - return f"video_{video_id}.mp4" - -# Usage -video_file = generate_and_download_video( - "A cat playing with a ball of yarn in a sunny garden" -) -``` - -### Error Handling - -```python -from litellm.exceptions import BadRequestError, AuthenticationError - -try: - response = video_generation( - prompt="A cat playing with a ball of yarn", - model="sora-2" - ) -except AuthenticationError as e: - print(f"Authentication failed: {e}") -except BadRequestError as e: - print(f"Bad request: {e}") -``` - -For more detailed documentation, see [Video Generation →](../video_generation.md) \ No newline at end of file +For detailed documentation on video generation, see [OpenAI Video Generation →](./openai/video_generation.md) \ No newline at end of file diff --git a/docs/my-website/docs/providers/openai/video_generation.md b/docs/my-website/docs/providers/openai/video_generation.md new file mode 100644 index 000000000000..4dc18da77bd2 --- /dev/null +++ b/docs/my-website/docs/providers/openai/video_generation.md @@ -0,0 +1,143 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# OpenAI Video Generation + +LiteLLM supports OpenAI's video generation models including Sora. + +## Quick Start + +### Required API Keys + +```python +import os +os.environ["OPENAI_API_KEY"] = "your-api-key" +``` + +### Basic Usage + +```python +from litellm import video_generation, video_retrieval +import os + +os.environ["OPENAI_API_KEY"] = "your-api-key" + +# Generate a video +response = video_generation( + prompt="A cat playing with a ball of yarn in a sunny garden", + model="sora-2", + seconds="8", + size="720x1280" +) + +print(f"Video ID: {response.id}") +print(f"Status: {response.status}") + +# Download video content when ready +video_bytes = video_retrieval( + video_id=response.id, + model="sora-2" +) + +# Save to file +with open("generated_video.mp4", "wb") as f: + f.write(video_bytes) +``` + +## Supported Models + +| Model Name | Description | Max Duration | Supported Sizes | +|------------|-------------|--------------|-----------------| +| sora-2 | OpenAI's latest video generation model | 8 seconds | 720x1280, 1280x720 | + +## Video Generation Parameters + +- `prompt` (required): Text description of the desired video +- `model` (optional): Model to use, defaults to "sora-2" +- `seconds` (optional): Video duration in seconds (e.g., "8", "16") +- `size` (optional): Video dimensions (e.g., "720x1280", "1280x720") +- `input_reference` (optional): Reference image for video editing +- `user` (optional): User identifier for tracking + +## Video Content Retrieval + +```python +# Download video content +video_bytes = video_retrieval( + video_id="video_1234567890", + model="sora-2" +) + +# Save to file +with open("video.mp4", "wb") as f: + f.write(video_bytes) +``` + +## Complete Workflow + +```python +import litellm +import time + +def generate_and_download_video(prompt): + # Step 1: Generate video + response = litellm.video_generation( + prompt=prompt, + model="sora-2", + seconds="8", + size="720x1280" + ) + + video_id = response.id + print(f"Video ID: {video_id}") + + # Step 2: Wait for processing (in practice, poll status) + time.sleep(30) + + # Step 3: Download video + video_bytes = litellm.video_retrieval( + video_id=video_id, + model="sora-2" + ) + + # Step 4: Save to file + with open(f"video_{video_id}.mp4", "wb") as f: + f.write(video_bytes) + + return f"video_{video_id}.mp4" + +# Usage +video_file = generate_and_download_video( + "A cat playing with a ball of yarn in a sunny garden" +) +``` + +## Video Editing with Reference Images + +```python +# Video editing with reference image +response = litellm.video_generation( + prompt="Make the cat jump higher", + input_reference="path/to/image.jpg", # Reference image + model="sora-2", + seconds="8" +) + +print(f"Video ID: {response.id}") +``` + +## Error Handling + +```python +from litellm.exceptions import BadRequestError, AuthenticationError + +try: + response = video_generation( + prompt="A cat playing with a ball of yarn", + model="sora-2" + ) +except AuthenticationError as e: + print(f"Authentication failed: {e}") +except BadRequestError as e: + print(f"Bad request: {e}") +``` diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index fc120fae85df..0ad1a702cafe 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -802,17 +802,23 @@ def completion_cost( # noqa: PLR0915 _usage = usage_obj if ResponseAPILoggingUtils._is_response_api_usage(_usage): - _usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( - _usage - ).model_dump() + # Skip token validation for video generation as it doesn't use token-based pricing + if call_type in [CallTypes.create_video.value, CallTypes.acreate_video.value]: + # For video generation, we'll handle cost calculation separately + pass + else: + _usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( + _usage + ).model_dump() # get input/output tokens from completion_response - prompt_tokens = _usage.get("prompt_tokens", 0) - completion_tokens = _usage.get("completion_tokens", 0) + # For video generation, these fields may be None, so handle gracefully + prompt_tokens = _usage.get("prompt_tokens", 0) or 0 + completion_tokens = _usage.get("completion_tokens", 0) or 0 cache_creation_input_tokens = _usage.get( "cache_creation_input_tokens", 0 - ) - cache_read_input_tokens = _usage.get("cache_read_input_tokens", 0) + ) or 0 + cache_read_input_tokens = _usage.get("cache_read_input_tokens", 0) or 0 if ( "prompt_tokens_details" in _usage and _usage["prompt_tokens_details"] != {} @@ -877,6 +883,29 @@ def completion_cost( # noqa: PLR0915 size=size, optional_params=optional_params, ) + elif ( + call_type == CallTypes.create_video.value + or call_type == CallTypes.acreate_video.value + ): + ### VIDEO GENERATION COST CALCULATION ### + if completion_response is not None and hasattr(completion_response, 'usage'): + usage_obj = completion_response.usage + duration_seconds = usage_obj.get('duration_seconds') + + if duration_seconds is not None: + # Calculate cost based on video duration using video-specific cost calculation + from litellm.llms.openai.cost_calculation import video_generation_cost + return video_generation_cost( + model=model, + duration_seconds=duration_seconds, + custom_llm_provider=custom_llm_provider + ) + # Fallback to default video cost calculation if no duration available + return default_video_cost_calculator( + model=model, + duration_seconds=0.0, # Default to 0 if no duration available + custom_llm_provider=custom_llm_provider + ) elif ( call_type == CallTypes.speech.value or call_type == CallTypes.aspeech.value @@ -1344,6 +1373,80 @@ def default_image_cost_calculator( return cost_info["input_cost_per_pixel"] * height * width * n +def default_video_cost_calculator( + model: str, + duration_seconds: float, + custom_llm_provider: Optional[str] = None, +) -> float: + """ + Default video cost calculator for video generation + + Args: + model (str): Model name + duration_seconds (float): Duration of the generated video in seconds + custom_llm_provider (Optional[str]): Custom LLM provider + + Returns: + float: Cost in USD for the video generation + + Raises: + Exception: If model pricing not found in cost map + """ + # Build model names for cost lookup + base_model_name = model + model_name_without_custom_llm_provider: Optional[str] = None + if custom_llm_provider and model.startswith(f"{custom_llm_provider}/"): + model_name_without_custom_llm_provider = model.replace( + f"{custom_llm_provider}/", "" + ) + base_model_name = f"{custom_llm_provider}/{model_name_without_custom_llm_provider}" + + verbose_logger.debug( + f"Looking up cost for video model: {base_model_name}" + ) + + model_without_provider = model.split('/')[-1] + + # Try model with provider first, fall back to base model name + cost_info: Optional[dict] = None + models_to_check: List[Optional[str]] = [ + base_model_name, + model, + model_without_provider, + model_name_without_custom_llm_provider, + ] + for _model in models_to_check: + if _model is not None and _model in litellm.model_cost: + cost_info = litellm.model_cost[_model] + break + + # If still not found, try with custom_llm_provider prefix + if cost_info is None and custom_llm_provider: + prefixed_model = f"{custom_llm_provider}/{model}" + if prefixed_model in litellm.model_cost: + cost_info = litellm.model_cost[prefixed_model] + if cost_info is None: + raise Exception( + f"Model not found in cost map. Tried checking {models_to_check}" + ) + + # Check for video-specific cost per second first + video_cost_per_second = cost_info.get("output_cost_per_video_per_second") + if video_cost_per_second is not None: + return video_cost_per_second * duration_seconds + + # Fallback to general output cost per second + output_cost_per_second = cost_info.get("output_cost_per_second") + if output_cost_per_second is not None: + return output_cost_per_second * duration_seconds + + # If no cost information found, return 0 + verbose_logger.warning( + f"No cost information found for video model {model}. Please add pricing to model_prices_and_context_window.json" + ) + return 0.0 + + def batch_cost_calculator( usage: Usage, model: str, diff --git a/litellm/llms/base_llm/video_retrieval/transformation.py b/litellm/llms/base_llm/video_retrieval/transformation.py deleted file mode 100644 index 9cb10540eeda..000000000000 --- a/litellm/llms/base_llm/video_retrieval/transformation.py +++ /dev/null @@ -1,89 +0,0 @@ -import types -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Optional, Union - -import httpx - - -if TYPE_CHECKING: - from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj - from litellm.types.videos.main import VideoResponse as _VideoResponse - - from ..chat.transformation import BaseLLMException as _BaseLLMException - - LiteLLMLoggingObj = _LiteLLMLoggingObj - BaseLLMException = _BaseLLMException - VideoResponse = _VideoResponse -else: - LiteLLMLoggingObj = Any - BaseLLMException = Any - VideoResponse = Any - - -class BaseVideoRetrievalConfig(ABC): - def __init__(self): - pass - - @classmethod - def get_config(cls): - return { - k: v - for k, v in cls.__dict__.items() - if not k.startswith("__") - and not k.startswith("_abc") - and not isinstance( - v, - ( - types.FunctionType, - types.BuiltinFunctionType, - classmethod, - staticmethod, - ), - ) - and v is not None - } - - @abstractmethod - def validate_environment( - self, - headers: dict, - model: str, - api_key: Optional[str] = None, - ) -> dict: - return {} - - @abstractmethod - def get_complete_url( - self, - model: str, - api_base: Optional[str], - litellm_params: dict, - ) -> str: - """ - Get the complete url for the request - - Some providers need `model` in `api_base` - """ - if api_base is None: - raise ValueError("api_base is required") - return api_base - - @abstractmethod - def transform_video_retrieve_response( - self, - model: str, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> bytes: - pass - - def get_error_class( - self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] - ) -> BaseLLMException: - from ..chat.transformation import BaseLLMException - - raise BaseLLMException( - status_code=status_code, - message=error_message, - headers=headers, - ) \ No newline at end of file diff --git a/litellm/llms/base_llm/videos_generation/transformation.py b/litellm/llms/base_llm/videos_generation/transformation.py deleted file mode 100644 index 61cc03bbf04d..000000000000 --- a/litellm/llms/base_llm/videos_generation/transformation.py +++ /dev/null @@ -1,119 +0,0 @@ -import types -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union - -import httpx -from httpx._types import RequestFiles - -from litellm.types.videos.main import VideoCreateOptionalRequestParams -from litellm.types.responses.main import * -from litellm.types.router import GenericLiteLLMParams - -if TYPE_CHECKING: - from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj - from litellm.types.videos.main import VideoResponse as _VideoResponse - - from ..chat.transformation import BaseLLMException as _BaseLLMException - - LiteLLMLoggingObj = _LiteLLMLoggingObj - BaseLLMException = _BaseLLMException - VideoResponse = _VideoResponse -else: - LiteLLMLoggingObj = Any - BaseLLMException = Any - VideoResponse = Any - - -class BaseVideoGenerationConfig(ABC): - def __init__(self): - pass - - @classmethod - def get_config(cls): - return { - k: v - for k, v in cls.__dict__.items() - if not k.startswith("__") - and not k.startswith("_abc") - and not isinstance( - v, - ( - types.FunctionType, - types.BuiltinFunctionType, - classmethod, - staticmethod, - ), - ) - and v is not None - } - - @abstractmethod - def get_supported_openai_params(self, model: str) -> list: - pass - - @abstractmethod - def map_openai_params( - self, - video_create_optional_params: VideoCreateOptionalRequestParams, - model: str, - drop_params: bool, - ) -> Dict: - pass - - @abstractmethod - def validate_environment( - self, - headers: dict, - model: str, - api_key: Optional[str] = None, - ) -> dict: - return {} - - @abstractmethod - def get_complete_url( - self, - model: str, - api_base: Optional[str], - litellm_params: dict, - ) -> str: - """ - OPTIONAL - - Get the complete url for the request - - Some providers need `model` in `api_base` - """ - if api_base is None: - raise ValueError("api_base is required") - return api_base - - @abstractmethod - def transform_video_create_request( - self, - model: str, - prompt: str, - video_create_optional_request_params: Dict, - litellm_params: GenericLiteLLMParams, - headers: dict, - ) -> Tuple[Dict, RequestFiles]: - pass - - @abstractmethod - def transform_video_create_response( - self, - model: str, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> VideoResponse: - pass - - def get_error_class( - self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] - ) -> BaseLLMException: - from ..chat.transformation import BaseLLMException - - raise BaseLLMException( - status_code=status_code, - message=error_message, - headers=headers, - ) diff --git a/litellm/llms/openai/cost_calculation.py b/litellm/llms/openai/cost_calculation.py index 229f75f26574..65a50224bb8d 100644 --- a/litellm/llms/openai/cost_calculation.py +++ b/litellm/llms/openai/cost_calculation.py @@ -120,3 +120,47 @@ def cost_per_second( completion_cost = 0.0 return prompt_cost, completion_cost + + +def video_generation_cost( + model: str, + duration_seconds: float, + custom_llm_provider: Optional[str] = None +) -> float: + """ + Calculates the cost for video generation based on duration in seconds. + + Input: + - model: str, the model name without provider prefix + - duration_seconds: float, the duration of the generated video in seconds + - custom_llm_provider: str, the custom llm provider + + Returns: + float - total_cost_in_usd + """ + ## GET MODEL INFO + model_info = get_model_info( + model=model, custom_llm_provider=custom_llm_provider or "openai" + ) + + # Check for video-specific cost per second + video_cost_per_second = model_info.get("output_cost_per_video_per_second") + if video_cost_per_second is not None: + verbose_logger.debug( + f"For model={model} - output_cost_per_video_per_second: {video_cost_per_second}; duration: {duration_seconds}" + ) + return video_cost_per_second * duration_seconds + + # Fallback to general output cost per second + output_cost_per_second = model_info.get("output_cost_per_second") + if output_cost_per_second is not None: + verbose_logger.debug( + f"For model={model} - output_cost_per_second: {output_cost_per_second}; duration: {duration_seconds}" + ) + return output_cost_per_second * duration_seconds + + # If no cost information found, return 0 + verbose_logger.warning( + f"No cost information found for video model {model}. Please add pricing to model_prices_and_context_window.json" + ) + return 0.0 diff --git a/litellm/llms/openai/video_generation/transformation.py b/litellm/llms/openai/video_generation/transformation.py deleted file mode 100644 index 55ff11159006..000000000000 --- a/litellm/llms/openai/video_generation/transformation.py +++ /dev/null @@ -1,183 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, IO -from io import BufferedReader - -import httpx -from httpx._types import RequestFiles - -from litellm.types.videos.main import VideoCreateOptionalRequestParams -from litellm.types.llms.openai import CreateVideoRequest -from litellm.types.videos.main import VideoResponse -from litellm.types.router import GenericLiteLLMParams -from litellm.secret_managers.main import get_secret_str -import litellm - -if TYPE_CHECKING: - from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj - - from ...base_llm.videos_generation.transformation import BaseVideoGenerationConfig as _BaseVideoGenerationConfig - from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException - - LiteLLMLoggingObj = _LiteLLMLoggingObj - BaseVideoGenerationConfig = _BaseVideoGenerationConfig - BaseLLMException = _BaseLLMException -else: - LiteLLMLoggingObj = Any - BaseVideoGenerationConfig = Any - BaseLLMException = Any - - -class OpenAIVideoGenerationConfig(BaseVideoGenerationConfig): - """ - Configuration class for OpenAI video generation. - """ - - def __init__(self): - super().__init__() - - def get_supported_openai_params(self, model: str) -> list: - """ - Get the list of supported OpenAI parameters for video generation. - """ - return [ - "model", - "prompt", - "input_reference", - "seconds", - "size", - "user", - "extra_headers", - ] - - def map_openai_params( - self, - video_create_optional_params: VideoCreateOptionalRequestParams, - model: str, - drop_params: bool, - ) -> Dict: - """No mapping applied since inputs are in OpenAI spec already""" - return dict(video_create_optional_params) - - def validate_environment( - self, - headers: dict, - model: str, - api_key: Optional[str] = None, - ) -> dict: - api_key = ( - api_key - or litellm.api_key - or litellm.openai_key - or get_secret_str("OPENAI_API_KEY") - ) - headers.update( - { - "Authorization": f"Bearer {api_key}", - } - ) - return headers - - def get_complete_url( - self, - model: str, - api_base: Optional[str], - litellm_params: dict, - ) -> str: - """ - Get the complete URL for OpenAI video generation. - """ - if api_base is None: - api_base = "https://api.openai.com/v1" - - return f"{api_base.rstrip('/')}/videos" - - def transform_video_create_request( - self, - model: str, - prompt: str, - video_create_optional_request_params: Dict, - litellm_params: GenericLiteLLMParams, - headers: dict, - ) -> Tuple[Dict, RequestFiles]: - """ - Transform the video creation request for OpenAI API. - """ - # Remove model and extra_headers from optional params as they're handled separately - video_create_optional_request_params = { - k: v for k, v in video_create_optional_request_params.items() - if k not in ["model", "extra_headers"] - } - - # Create the request data - video_create_request = CreateVideoRequest( - model=model, - prompt=prompt, - **video_create_optional_request_params - ) - - # Handle file uploads - files_list: List[Tuple[str, Tuple[str, Union[IO[bytes], bytes, str], str]]] = [] - - # Handle input_reference parameter if provided - _input_reference = video_create_optional_request_params.get("input_reference") - if _input_reference is not None: - if isinstance(_input_reference, BufferedReader): - files_list.append( - ("input_reference", (_input_reference.name, _input_reference, "image/png")) - ) - elif isinstance(_input_reference, str): - # Handle file path - open the file - try: - with open(_input_reference, "rb") as f: - files_list.append( - ("input_reference", (f.name, f.read(), "image/png")) - ) - except Exception as e: - raise ValueError(f"Could not open input_reference file {_input_reference}: {e}") - else: - # Handle file-like object - files_list.append( - ("input_reference", ("input_reference.png", _input_reference, "image/png")) - ) - - # Convert to dict for JSON serialization - data = dict(video_create_request) - - return data, files_list - - def transform_video_create_response( - self, - model: str, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> VideoResponse: - """ - Transform the OpenAI video creation response. - """ - response_data = raw_response.json() - - # Transform the response data - from litellm.types.videos.main import VideoObject - video_objects = [] - for video_data in response_data.get("data", []): - video_obj = VideoObject(**video_data) - video_objects.append(video_obj) - - # Create the response - response = VideoResponse( - data=video_objects, - usage=response_data.get("usage", {}), - hidden_params={}, - ) - - return response - - def get_error_class( - self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] - ) -> BaseLLMException: - from ...base_llm.chat.transformation import BaseLLMException - - raise BaseLLMException( - status_code=status_code, - message=error_message, - headers=headers, - ) diff --git a/litellm/llms/openai/video_retrieval/transformation.py b/litellm/llms/openai/video_retrieval/transformation.py deleted file mode 100644 index a170fe3e1169..000000000000 --- a/litellm/llms/openai/video_retrieval/transformation.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import TYPE_CHECKING, Any, Optional, Union - -import httpx - -from litellm.secret_managers.main import get_secret_str -import litellm - -if TYPE_CHECKING: - from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj - from litellm.types.videos.main import VideoResponse as _VideoResponse - - from ...base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig as _BaseVideoRetrievalConfig - from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException - - LiteLLMLoggingObj = _LiteLLMLoggingObj - BaseVideoRetrievalConfig = _BaseVideoRetrievalConfig - BaseLLMException = _BaseLLMException - VideoResponse = _VideoResponse -else: - LiteLLMLoggingObj = Any - BaseVideoRetrievalConfig = Any - BaseLLMException = Any - VideoResponse = Any - - -class OpenAIVideoRetrievalConfig(BaseVideoRetrievalConfig): - """ - Configuration class for OpenAI video retrieval operations. - """ - - def __init__(self): - super().__init__() - - def validate_environment( - self, - headers: dict, - model: str, - api_key: Optional[str] = None, - ) -> dict: - api_key = ( - api_key - or litellm.api_key - or litellm.openai_key - or get_secret_str("OPENAI_API_KEY") - ) - headers.update( - { - "Authorization": f"Bearer {api_key}", - } - ) - return headers - - - def get_complete_url( - self, - model: str, - api_base: Optional[str], - litellm_params: dict, - ) -> str: - """ - Get the complete URL for OpenAI video operations. - For video content download, this returns the base videos URL. - """ - if api_base is None: - api_base = "https://api.openai.com/v1" - - return f"{api_base.rstrip('/')}/videos" - - def transform_video_retrieve_response( - self, - model: str, - raw_response: httpx.Response, - logging_obj: LiteLLMLoggingObj, - ) -> bytes: - """ - Transform the OpenAI video content download response. - Returns raw video content as bytes. - """ - # For video content download, return the raw content as bytes - return raw_response.content - - def get_error_class( - self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] - ) -> BaseLLMException: - from ...base_llm.chat.transformation import BaseLLMException - - raise BaseLLMException( - status_code=status_code, - message=error_message, - headers=headers, - ) diff --git a/litellm/types/videos/main.py b/litellm/types/videos/main.py index 283c6ca7ad94..f33e1d5a4411 100644 --- a/litellm/types/videos/main.py +++ b/litellm/types/videos/main.py @@ -19,6 +19,7 @@ class VideoObject(BaseModel): seconds: Optional[str] = None size: Optional[str] = None model: Optional[str] = None + usage: Optional[Dict[str, Any]] = None _hidden_params: Dict[str, Any] = {} def __contains__(self, key): @@ -41,35 +42,11 @@ def json(self, **kwargs): # type: ignore return self.dict() -class VideoUsage(BaseModel): - """Usage information for video generation.""" - input_tokens: Optional[int] = None - output_tokens: Optional[int] = None - total_tokens: Optional[int] = None - duration_seconds: Optional[float] = None - resolution: Optional[str] = None - _hidden_params: Dict[str, Any] = {} - - def __contains__(self, key): - return hasattr(self, key) - - def get(self, key, default=None): - return getattr(self, key, default) - - def __getitem__(self, key): - return getattr(self, key) - - def json(self, **kwargs): # type: ignore - try: - return self.model_dump(**kwargs) - except Exception: - return self.dict() class VideoResponse(BaseModel): """Response object for video generation requests.""" data: List[VideoObject] - usage: VideoUsage hidden_params: Dict[str, Any] = {} def __contains__(self, key): diff --git a/litellm/utils.py b/litellm/utils.py index 1b15c98fa1d9..e8cc39a48de9 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -262,8 +262,7 @@ from litellm.llms.base_llm.image_variations.transformation import ( BaseImageVariationConfig, ) -from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig -from litellm.llms.base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig +from litellm.llms.base_llm.videos.transformation import BaseVideoConfig from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig @@ -4951,6 +4950,7 @@ def _get_model_info_helper( # noqa: PLR0915 "output_cost_per_token_above_200k_tokens", None ), output_cost_per_second=_model_info.get("output_cost_per_second", None), + output_cost_per_video_per_second=_model_info.get("output_cost_per_video_per_second", None), output_cost_per_image=_model_info.get("output_cost_per_image", None), output_vector_size=_model_info.get("output_vector_size", None), citation_cost_per_token=_model_info.get( @@ -7554,30 +7554,18 @@ def get_provider_image_generation_config( return None @staticmethod - def get_provider_video_generation_config( + def get_provider_video_config( model: str, provider: LlmProviders, - ) -> Optional[BaseVideoGenerationConfig]: + ) -> Optional[BaseVideoConfig]: if LlmProviders.OPENAI == provider: - from litellm.llms.openai.video_generation.transformation import ( - OpenAIVideoGenerationConfig, + from litellm.llms.openai.videos.transformation import ( + OpenAIVideoConfig, ) - return OpenAIVideoGenerationConfig() + return OpenAIVideoConfig() return None - @staticmethod - def get_provider_video_content_config( - model: str, - provider: LlmProviders, - ) -> Optional[BaseVideoRetrievalConfig]: - if LlmProviders.OPENAI == provider: - from litellm.llms.openai.video_retrieval.transformation import ( - OpenAIVideoRetrievalConfig, - ) - - return OpenAIVideoRetrievalConfig() - return None @staticmethod def get_provider_realtime_config( diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index c6f8275a6a12..f5c3b4952ecc 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -23724,5 +23724,21 @@ "supports_tool_choice": true, "supports_vision": true, "supports_web_search": true + }, + "openai/sora-2": { + "litellm_provider": "openai", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.10, + "source": "https://platform.openai.com/docs/api-reference/videos", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "720x1280", + "1280x720" + ] } } From a92b6b96ad1b18f2a6ea86be6eda29ff375b0e0a Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 15 Oct 2025 23:40:33 +0530 Subject: [PATCH 08/17] remove mock code --- litellm/llms/custom_httpx/llm_http_handler.py | 74 ++++++------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 04193d53690f..f2dd8d90cd46 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -44,8 +44,7 @@ from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig -from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig -from litellm.llms.base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig +from litellm.llms.base_llm.videos.transformation import BaseVideoConfig from litellm.llms.custom_httpx.http_handler import ( AsyncHTTPHandler, HTTPHandler, @@ -83,7 +82,7 @@ VectorStoreSearchOptionalRequestParams, VectorStoreSearchResponse, ) -from litellm.types.videos.main import VideoResponse +from litellm.types.videos.main import VideoObject from litellm.utils import ( CustomStreamWrapper, ImageResponse, @@ -3285,6 +3284,7 @@ def _handle_error( BaseOCRConfig, BaseVideoGenerationConfig, "BaseVideoRetrievalConfig", + BaseVideoConfig, "BasePassthroughConfig", ], ): @@ -3792,7 +3792,7 @@ def video_generation_handler( self, model: str, prompt: str, - video_generation_provider_config: "BaseVideoGenerationConfig", + video_generation_provider_config: BaseVideoConfig, video_generation_optional_request_params: Dict, custom_llm_provider: str, litellm_params: GenericLiteLLMParams, @@ -3806,8 +3806,8 @@ def video_generation_handler( litellm_metadata: Optional[Dict[str, Any]] = None, api_key: Optional[str] = None, ) -> Union[ - VideoResponse, - Coroutine[Any, Any, VideoResponse], + VideoObject, + Coroutine[Any, Any, VideoObject], ]: """ Handles video generation requests. @@ -3876,36 +3876,7 @@ def video_generation_handler( try: # Use JSON when no files, otherwise use form data with files - # if files is None or len(files) == 0: - # # --- BEGIN MOCK VIDEO RESPONSE --- - # mock_video_response = { - # "data": [ - # { - # "id": "video_123", - # "object": "video", - # "model": "sora-2", - # "status": "queued", - # "progress": 0, - # "created_at": 1712697600, - # "size": "1024x1808", - # "seconds": "8", - # "quality": "standard", - # } - # ], - # "usage": {}, - # "hidden_params": {}, - # } - - # import types - # class MockHTTPXResponse: - # def __init__(self, json_data): - # self._json_data = json_data - # self.status_code = 200 - # self.text = str(json_data) - # def json(self): - # return self._json_data - # response = MockHTTPXResponse(mock_video_response) - if files: + if files and len(files) > 0: # Use multipart/form-data when files are present response = sync_httpx_client.post( url=api_base, @@ -3920,8 +3891,7 @@ def video_generation_handler( response = sync_httpx_client.post( url=api_base, headers=headers, - data=data, - files=files, + json=data, timeout=timeout, ) @@ -3941,7 +3911,7 @@ async def async_video_generation_handler( self, model: str, prompt: str, - video_generation_provider_config: "BaseVideoGenerationConfig", + video_generation_provider_config: "BaseVideoConfig", video_generation_optional_request_params: Dict, custom_llm_provider: str, litellm_params: GenericLiteLLMParams, @@ -3953,7 +3923,7 @@ async def async_video_generation_handler( fake_stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, api_key: Optional[str] = None, - ) -> VideoResponse: + ) -> VideoObject: """ Async version of the video generation handler. Uses async HTTP client to make requests. @@ -4036,7 +4006,7 @@ def video_content_handler( self, video_id: str, model: str, - video_content_provider_config: "BaseVideoRetrievalConfig", + video_content_provider_config: BaseVideoConfig, custom_llm_provider: str, litellm_params: GenericLiteLLMParams, logging_obj: LiteLLMLoggingObj, @@ -4062,7 +4032,7 @@ def video_content_handler( api_key=api_key, client=client, ) - + if client is None or not isinstance(client, HTTPHandler): sync_httpx_client = _get_httpx_client( params={"ssl_verify": litellm_params.get("ssl_verify", None)} @@ -4087,10 +4057,10 @@ def video_content_handler( # Construct the URL for video content download url = f"{api_base.rstrip('/')}/{video_id}/content" - + # Add variant query parameter if provided params = { "video_id": video_id } - + try: # Make the GET request to download content response = sync_httpx_client.get( @@ -4098,14 +4068,14 @@ def video_content_handler( headers=headers, params=params, ) - + # Transform the response using the provider config - return video_content_provider_config.transform_video_retrieve_response( + return video_content_provider_config.transform_video_content_response( model=model, raw_response=response, logging_obj=logging_obj, ) - + except Exception as e: raise self._handle_error( e=e, @@ -4116,7 +4086,7 @@ async def async_video_content_handler( self, video_id: str, model: str, - video_content_provider_config: "BaseVideoRetrievalConfig", + video_content_provider_config: BaseVideoConfig, custom_llm_provider: str, litellm_params: GenericLiteLLMParams, logging_obj: LiteLLMLoggingObj, @@ -4153,7 +4123,7 @@ async def async_video_content_handler( # Construct the URL for video content download url = f"{api_base.rstrip('/')}/{video_id}/content" - + params = { "video_id": video_id, } @@ -4165,14 +4135,14 @@ async def async_video_content_handler( headers=headers, params=params, ) - + # Transform the response using the provider config - return video_content_provider_config.transform_video_retrieve_response( + return video_content_provider_config.transform_video_content_response( model=model, raw_response=response, logging_obj=logging_obj, ) - + except Exception as e: raise self._handle_error( e=e, From 7c589ccfa9c89448777b9ee6f00f001a8f31f159 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 15 Oct 2025 23:43:07 +0530 Subject: [PATCH 09/17] remove not required comments --- litellm/cost_calculator.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 0ad1a702cafe..24db4fb7d099 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -812,13 +812,12 @@ def completion_cost( # noqa: PLR0915 ).model_dump() # get input/output tokens from completion_response - # For video generation, these fields may be None, so handle gracefully - prompt_tokens = _usage.get("prompt_tokens", 0) or 0 - completion_tokens = _usage.get("completion_tokens", 0) or 0 + prompt_tokens = _usage.get("prompt_tokens", 0) + completion_tokens = _usage.get("completion_tokens", 0) cache_creation_input_tokens = _usage.get( "cache_creation_input_tokens", 0 - ) or 0 - cache_read_input_tokens = _usage.get("cache_read_input_tokens", 0) or 0 + ) + cache_read_input_tokens = _usage.get("cache_read_input_tokens", 0) if ( "prompt_tokens_details" in _usage and _usage["prompt_tokens_details"] != {} From dc4b805e38b206f2bf248064413d8ceb45551e87 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 15 Oct 2025 23:48:21 +0530 Subject: [PATCH 10/17] Add tests --- tests/test_litellm/test_video_generation.py | 382 ++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 tests/test_litellm/test_video_generation.py diff --git a/tests/test_litellm/test_video_generation.py b/tests/test_litellm/test_video_generation.py new file mode 100644 index 000000000000..109f8629510a --- /dev/null +++ b/tests/test_litellm/test_video_generation.py @@ -0,0 +1,382 @@ +import json +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.types.videos.main import VideoObject, VideoResponse +from litellm.videos.main import video_generation, avideo_generation +from litellm.llms.openai.videos.transformation import OpenAIVideoConfig +from litellm.cost_calculator import default_video_cost_calculator + + +class TestVideoGeneration: + """Test suite for video generation functionality.""" + + def test_video_generation_basic(self): + """Test basic video generation functionality.""" + # Mock the video generation response + mock_response = VideoObject( + id="video_123", + object="video", + status="queued", + created_at=1712697600, + model="sora-2", + size="720x1280", + seconds="8" + ) + + with patch('litellm.videos.main.base_llm_http_handler') as mock_handler: + mock_handler.video_generation_handler.return_value = mock_response + + response = video_generation( + prompt="Show them running around the room", + model="sora-2", + seconds="8", + size="720x1280" + ) + + assert isinstance(response, VideoObject) + assert response.id == "video_123" + 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 + ) + + with patch('litellm.videos.main.base_llm_http_handler') as mock_handler: + mock_handler.video_generation_handler.return_value = mock_response + + 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('litellm.videos.main.base_llm_http_handler') as mock_handler: + mock_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 = config.transform_video_create_request( + model="sora-2", + prompt="Test video prompt", + 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 == [] + + 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.""" + # Load the local model cost map instead of online + import json + with open("model_prices_and_context_window.json", "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_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 = config.transform_video_create_request( + model="sora-2", + prompt="Test video with image", + 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_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 unsupported parameters.""" + from litellm.videos.utils import VideoGenerationRequestUtils + + # Test unsupported parameter detection + with pytest.raises(litellm.UnsupportedParamsError): + VideoGenerationRequestUtils.get_optional_params_video_generation( + model="sora-2", + video_generation_provider_config=OpenAIVideoConfig(), + video_generation_optional_params={ + "unsupported_param": "value" + } + ) + + def test_video_generation_request_utils(self): + """Test video generation request utilities.""" + from litellm.videos.utils import VideoGenerationRequestUtils + + # Test parameter filtering + params = { + "prompt": "Test video", + "model": "sora-2", + "seconds": "8", + "size": "720x1280", + "user": "test-user", + "invalid_param": "should_be_filtered" + } + + filtered_params = VideoGenerationRequestUtils.get_requested_video_generation_optional_param(params) + + # Should only contain valid parameters + assert "prompt" not in filtered_params # prompt is required, not optional + assert "seconds" in filtered_params + assert "size" in filtered_params + assert "user" in filtered_params + assert "invalid_param" not in filtered_params + # Note: model is included in the filtered params as it's part of the TypedDict + + 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" + + +if __name__ == "__main__": + pytest.main([__file__]) From 5f030f5ca62e395c74c4b25a8bbf804981607896 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 15 Oct 2025 23:51:59 +0530 Subject: [PATCH 11/17] Add tests --- litellm/videos/main.py | 41 +++++++++++++++++++---------------------- litellm/videos/utils.py | 9 ++++++--- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/litellm/videos/main.py b/litellm/videos/main.py index ba5f870d5b9f..c77581b5b575 100644 --- a/litellm/videos/main.py +++ b/litellm/videos/main.py @@ -10,17 +10,15 @@ VideoCreateOptionalRequestParams, VideoObject, VideoResponse, - VideoUsage, ) from litellm.videos.utils import VideoGenerationRequestUtils from litellm.constants import DEFAULT_VIDEO_ENDPOINT_MODEL, request_timeout as DEFAULT_REQUEST_TIMEOUT from litellm.main import base_llm_http_handler from litellm.utils import client, ProviderConfigManager -from litellm.types.utils import FileTypes +from litellm.types.utils import FileTypes, CallTypes from litellm.types.router import GenericLiteLLMParams from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj -from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig -from litellm.llms.base_llm.video_retrieval.transformation import BaseVideoRetrievalConfig +from litellm.llms.base_llm.videos.transformation import BaseVideoConfig from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler #################### Initialize provider clients #################### @@ -43,7 +41,7 @@ async def avideo_generation( extra_query: Optional[Dict[str, Any]] = None, extra_body: Optional[Dict[str, Any]] = None, **kwargs, -) -> VideoResponse: +) -> VideoObject: """ Asynchronously calls the `video_generation` function with the given arguments and keyword arguments. @@ -129,7 +127,7 @@ def video_generation( *, avideo_generation: Literal[True], **kwargs, -) -> Coroutine[Any, Any, VideoResponse]: +) -> Coroutine[Any, Any, VideoObject]: ... @@ -149,7 +147,7 @@ def video_generation( *, avideo_generation: Literal[False] = False, **kwargs, -) -> VideoResponse: +) -> VideoObject: ... # fmt: on @@ -172,8 +170,8 @@ def video_generation( # noqa: PLR0915 extra_body: Optional[Dict[str, Any]] = None, **kwargs, ) -> Union[ - VideoResponse, - Coroutine[Any, Any, VideoResponse], + VideoObject, + Coroutine[Any, Any, VideoObject], ]: """ Maps the https://api.openai.com/v1/videos endpoint. @@ -191,12 +189,8 @@ def video_generation( # noqa: PLR0915 if mock_response is not None: if isinstance(mock_response, str): mock_response = json.loads(mock_response) - - response = VideoResponse( - data=[VideoObject(**video_data) for video_data in mock_response.get("data", [])], - usage=VideoUsage(**mock_response.get("usage", {})), - hidden_params=kwargs.get("hidden_params", {}), - ) + + response = VideoObject(**mock_response) return response # get llm provider logic @@ -207,8 +201,8 @@ def video_generation( # noqa: PLR0915 ) # get provider config - video_generation_provider_config: Optional[BaseVideoGenerationConfig] = ( - ProviderConfigManager.get_provider_video_generation_config( + video_generation_provider_config: Optional[BaseVideoConfig] = ( + ProviderConfigManager.get_provider_video_config( model=model, provider=litellm.LlmProviders(custom_llm_provider), ) @@ -244,6 +238,9 @@ def video_generation( # noqa: PLR0915 custom_llm_provider=custom_llm_provider, ) + # Set the correct call type for video generation + litellm_logging_obj.call_type = CallTypes.create_video.value + # Call the handler with _is_async flag instead of directly calling the async handler return base_llm_http_handler.video_generation_handler( model=model, @@ -334,15 +331,15 @@ def video_content( ) # get provider config - video_content_provider_config: Optional[BaseVideoRetrievalConfig] = ( - ProviderConfigManager.get_provider_video_content_config( + video_provider_config: Optional[BaseVideoConfig] = ( + ProviderConfigManager.get_provider_video_config( model=model, provider=litellm.LlmProviders(custom_llm_provider), ) ) - if video_content_provider_config is None: - raise ValueError(f"video content download is not supported for {custom_llm_provider}") + if video_provider_config is None: + raise ValueError(f"video support download is not supported for {custom_llm_provider}") local_vars.update(kwargs) # For video content download, we don't need complex optional parameter handling @@ -367,7 +364,7 @@ def video_content( return base_llm_http_handler.video_content_handler( video_id=video_id, model=model, - video_content_provider_config=video_content_provider_config, + video_content_provider_config=video_provider_config, custom_llm_provider=custom_llm_provider, litellm_params=litellm_params, logging_obj=litellm_logging_obj, diff --git a/litellm/videos/utils.py b/litellm/videos/utils.py index 2e7e025927c0..7cfccab6720d 100644 --- a/litellm/videos/utils.py +++ b/litellm/videos/utils.py @@ -1,7 +1,7 @@ from typing import Any, Dict, cast, get_type_hints import litellm -from litellm.llms.base_llm.videos_generation.transformation import BaseVideoGenerationConfig +from litellm.llms.base_llm.videos.transformation import BaseVideoConfig from litellm.types.videos.main import VideoCreateOptionalRequestParams @@ -11,7 +11,7 @@ class VideoGenerationRequestUtils: @staticmethod def get_optional_params_video_generation( model: str, - video_generation_provider_config: BaseVideoGenerationConfig, + video_generation_provider_config: BaseVideoConfig, video_generation_optional_params: VideoCreateOptionalRequestParams, ) -> Dict: """ @@ -38,7 +38,10 @@ def get_optional_params_video_generation( if unsupported_params: raise litellm.UnsupportedParamsError( model=model, - message=f"The following parameters are not supported for model {model}: {', '.join(unsupported_params)}", + message=( + f"The following parameters are not supported for model {model}: " + f"{', '.join(unsupported_params)}" + ), ) # Map parameters to provider-specific format From b3977094f535d2f8fca84e38a11720f30e7c480d Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 16 Oct 2025 17:29:14 +0530 Subject: [PATCH 12/17] Add other video endpoints --- litellm/llms/custom_httpx/llm_http_handler.py | 345 ++++++++++ litellm/types/utils.py | 8 + litellm/videos/main.py | 650 ++++++++++++++++++ 3 files changed, 1003 insertions(+) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index f2dd8d90cd46..5d0e20ee625c 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -4148,6 +4148,351 @@ async def async_video_content_handler( e=e, provider_config=video_content_provider_config, ) + + def video_remix_handler( + self, + video_id: str, + prompt: str, + model: str, + video_remix_provider_config: BaseVideoConfig, + custom_llm_provider: str, + litellm_params, + logging_obj, + extra_headers: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[float] = None, + _is_async: bool = False, + client=None, + ): + """ + Handler for video remix requests. + """ + if _is_async: + return self.async_video_remix_handler( + video_id=video_id, + prompt=prompt, + model=model, + video_remix_provider_config=video_remix_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout, + client=client, + ) + else: + # For sync calls, we'll use the async handler in a sync context + import asyncio + return asyncio.run( + self.async_video_remix_handler( + video_id=video_id, + prompt=prompt, + model=model, + video_remix_provider_config=video_remix_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout, + client=client, + ) + ) + + async def async_video_remix_handler( + self, + video_id: str, + prompt: str, + model: str, + video_remix_provider_config: BaseVideoConfig, + custom_llm_provider: str, + litellm_params, + logging_obj, + extra_headers: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[float] = None, + client=None, + ): + """ + Async version of the video remix handler. + """ + if client is None or not isinstance(client, AsyncHTTPHandler): + async_httpx_client = get_async_httpx_client( + llm_provider=litellm.LlmProviders(custom_llm_provider), + params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + ) + else: + async_httpx_client = client + + headers = video_remix_provider_config.validate_environment( + api_key=litellm_params.get("api_key"), + headers=extra_headers or {}, + model=model, + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = video_remix_provider_config.get_complete_url( + model=model, + api_base=litellm_params.get("api_base", None), + litellm_params=dict(litellm_params), + ) + + # Construct the URL for video remix + url = f"{api_base.rstrip('/')}/{video_id}/remix" + + # Prepare the request data + data = { + "prompt": prompt, + } + + # Add any extra body parameters + if extra_body: + data.update(extra_body) + + ## LOGGING + logging_obj.pre_call( + input=prompt, + api_key="", + additional_args={ + "complete_input_dict": data, + "api_base": url, + "headers": headers, + "video_id": video_id, + }, + ) + + try: + response = await async_httpx_client.post( + url=url, + headers=headers, + json=data, + timeout=timeout, + ) + + return video_remix_provider_config.transform_video_remix_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + + except Exception as e: + raise self._handle_error( + e=e, + provider_config=video_remix_provider_config, + ) + + def video_list_handler( + self, + after: Optional[str], + limit: Optional[int], + order: Optional[str], + model: str, + video_list_provider_config, + custom_llm_provider: str, + litellm_params, + logging_obj, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + timeout: Optional[float] = None, + _is_async: bool = False, + client=None, + ): + """ + Handler for video list requests. + """ + if _is_async: + return self.async_video_list_handler( + after=after, + limit=limit, + order=order, + model=model, + video_list_provider_config=video_list_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + extra_headers=extra_headers, + extra_query=extra_query, + timeout=timeout, + client=client, + ) + else: + # For sync calls, we'll use the async handler in a sync context + import asyncio + return asyncio.run( + self.async_video_list_handler( + after=after, + limit=limit, + order=order, + model=model, + video_list_provider_config=video_list_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=logging_obj, + extra_headers=extra_headers, + extra_query=extra_query, + timeout=timeout, + client=client, + ) + ) + + async def async_video_list_handler( + self, + after: Optional[str], + limit: Optional[int], + order: Optional[str], + model: str, + video_list_provider_config: BaseVideoConfig, + custom_llm_provider: str, + litellm_params, + logging_obj, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + timeout: Optional[float] = None, + client=None, + ): + """ + Async version of the video list handler. + """ + if client is None or not isinstance(client, AsyncHTTPHandler): + async_httpx_client = get_async_httpx_client( + llm_provider=litellm.LlmProviders(custom_llm_provider), + params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + ) + else: + async_httpx_client = client + + headers = video_list_provider_config.validate_environment( + api_key=litellm_params.get("api_key"), + headers=extra_headers or {}, + model=model, + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = video_list_provider_config.get_complete_url( + model=model, + api_base=litellm_params.get("api_base", None), + litellm_params=dict(litellm_params), + ) + + # Prepare query parameters + params = {} + if after is not None: + params["after"] = after + if limit is not None: + params["limit"] = limit + if order is not None: + params["order"] = order + + # Add any extra query parameters + if extra_query: + params.update(extra_query) + + ## LOGGING + logging_obj.pre_call( + input="", + api_key="", + additional_args={ + "api_base": api_base, + "headers": headers, + "params": params, + }, + ) + + try: + response = await async_httpx_client.get( + url=api_base, + headers=headers, + params=params, + timeout=timeout, + ) + + return video_list_provider_config.transform_video_list_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + + except Exception as e: + raise self._handle_error( + e=e, + provider_config=video_list_provider_config, + ) + + async def async_video_delete_handler( + self, + video_id: str, + model: str, + video_delete_provider_config: BaseVideoConfig, + custom_llm_provider: str, + litellm_params, + logging_obj, + extra_headers: Optional[Dict[str, Any]] = None, + timeout: Optional[float] = None, + client=None, + ): + """ + Async version of the video delete handler. + """ + if client is None or not isinstance(client, AsyncHTTPHandler): + async_httpx_client = get_async_httpx_client( + llm_provider=litellm.LlmProviders(custom_llm_provider), + params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + ) + else: + async_httpx_client = client + + headers = video_delete_provider_config.validate_environment( + api_key=litellm_params.get("api_key"), + headers=extra_headers or {}, + model=model, + ) + + if extra_headers: + headers.update(extra_headers) + + api_base = video_delete_provider_config.get_complete_url( + model=model, + api_base=litellm_params.get("api_base", None), + litellm_params=dict(litellm_params), + ) + + # Construct the URL for video delete + url = f"{api_base.rstrip('/')}/{video_id}" + + ## LOGGING + logging_obj.pre_call( + input="", + api_key="", + additional_args={ + "api_base": url, + "headers": headers, + "video_id": video_id, + }, + ) + + try: + response = await async_httpx_client.delete( + url=url, + headers=headers, + timeout=timeout, + ) + + return video_delete_provider_config.transform_video_delete_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + + except Exception as e: + raise self._handle_error( + e=e, + provider_config=video_delete_provider_config, + ) ###### VECTOR STORE HANDLER ###### async def async_vector_store_search_handler( diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 6f787c8ce9b3..e16e0ae5b468 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -286,6 +286,14 @@ class CallTypes(str, Enum): video_retrieve = "video_retrieve" avideo_content = "avideo_content" video_content = "video_content" + video_remix = "video_remix" + avideo_remix = "avideo_remix" + video_list = "video_list" + avideo_list = "avideo_list" + video_retrieve_job = "video_retrieve_job" + avideo_retrieve_job = "avideo_retrieve_job" + video_delete = "video_delete" + avideo_delete = "avideo_delete" acancel_fine_tuning_job = "acancel_fine_tuning_job" cancel_fine_tuning_job = "cancel_fine_tuning_job" alist_fine_tuning_jobs = "alist_fine_tuning_jobs" diff --git a/litellm/videos/main.py b/litellm/videos/main.py index c77581b5b575..eb6e0cf52bef 100644 --- a/litellm/videos/main.py +++ b/litellm/videos/main.py @@ -461,3 +461,653 @@ async def avideo_content( completion_kwargs=local_vars, extra_kwargs=kwargs, ) + +##### Video Remix ####################### +@client +async def avideo_remix( + video_id: str, + prompt: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> VideoObject: + """ + Asynchronously calls the `video_remix` function with the given arguments and keyword arguments. + + Parameters: + - `video_id` (str): The identifier of the completed video to remix + - `prompt` (str): Updated text prompt that directs the remix generation + - `model` (Optional[str]): The video generation model to use + - `timeout` (int): Request timeout in seconds + - `custom_llm_provider` (Optional[str]): The LLM provider to use + - `extra_headers` (Optional[Dict[str, Any]]): Additional headers + - `extra_query` (Optional[Dict[str, Any]]): Additional query parameters + - `extra_body` (Optional[Dict[str, Any]]): Additional body parameters + - `kwargs` (dict): Additional keyword arguments + + Returns: + - `response` (VideoObject): The response returned by the `video_remix` function. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["async_call"] = True + + # get custom llm provider so we can use this for mapping exceptions + if custom_llm_provider is None: + _, custom_llm_provider, _, _ = litellm.get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, api_base=local_vars.get("api_base", None) + ) + + func = partial( + video_remix, + video_id=video_id, + prompt=prompt, + model=model, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +# fmt: off + +# Overload for when avideo_remix=True (returns Coroutine) +@overload +def video_remix( + video_id: str, + prompt: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_remix: Literal[True], + **kwargs, +) -> Coroutine[Any, Any, VideoObject]: + ... + + +@overload +def video_remix( + video_id: str, + prompt: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_remix: Literal[False] = False, + **kwargs, +) -> VideoObject: + ... + +# fmt: on + + +@client +def video_remix( # noqa: PLR0915 + video_id: str, + prompt: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> Union[ + VideoObject, + Coroutine[Any, Any, VideoObject], +]: + """ + Maps the https://api.openai.com/v1/videos/{video_id}/remix endpoint. + + Currently supports OpenAI + """ + local_vars = locals() + try: + litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore + litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None) + _is_async = kwargs.pop("async_call", False) is True + + # Check for mock response first + mock_response = kwargs.get("mock_response", None) + if mock_response is not None: + if isinstance(mock_response, str): + mock_response = json.loads(mock_response) + + response = VideoObject(**mock_response) + return response + + # get llm provider logic + litellm_params = GenericLiteLLMParams(**kwargs) + model, custom_llm_provider, _, _ = get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, + custom_llm_provider=custom_llm_provider, + ) + + # get provider config + video_remix_provider_config: Optional[BaseVideoConfig] = ( + ProviderConfigManager.get_provider_video_config( + model=model, + provider=litellm.LlmProviders(custom_llm_provider), + ) + ) + + if video_remix_provider_config is None: + raise ValueError(f"video remix is not supported for {custom_llm_provider}") + + local_vars.update(kwargs) + # For video remix, we need the video_id and prompt + video_remix_request_params: Dict = { + "video_id": video_id, + "prompt": prompt, + } + + # Pre Call logging + litellm_logging_obj.update_environment_variables( + model=model, + user=kwargs.get("user"), + optional_params=dict(video_remix_request_params), + litellm_params={ + "litellm_call_id": litellm_call_id, + **video_remix_request_params, + }, + custom_llm_provider=custom_llm_provider, + ) + + # Set the correct call type for video remix + litellm_logging_obj.call_type = CallTypes.video_remix.value + + # Call the handler with _is_async flag instead of directly calling the async handler + return base_llm_http_handler.video_remix_handler( + video_id=video_id, + prompt=prompt, + model=model, + video_remix_provider_config=video_remix_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=litellm_logging_obj, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout or DEFAULT_REQUEST_TIMEOUT, + _is_async=_is_async, + client=kwargs.get("client"), + ) + + except Exception as e: + raise litellm.exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +##### Video List ####################### +@client +async def avideo_list( + after: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> Dict[str, Any]: + """ + Asynchronously calls the `video_list` function with the given arguments and keyword arguments. + + Parameters: + - `after` (Optional[str]): Identifier for the last item from the previous pagination request + - `limit` (Optional[int]): Number of items to retrieve + - `order` (Optional[str]): Sort order of results by timestamp. Use asc for ascending order or desc for descending order + - `model` (Optional[str]): The video generation model to use + - `timeout` (int): Request timeout in seconds + - `custom_llm_provider` (Optional[str]): The LLM provider to use + - `extra_headers` (Optional[Dict[str, Any]]): Additional headers + - `extra_query` (Optional[Dict[str, Any]]): Additional query parameters + - `extra_body` (Optional[Dict[str, Any]]): Additional body parameters + - `kwargs` (dict): Additional keyword arguments + + Returns: + - `response` (Dict[str, Any]): The response returned by the `video_list` function. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["async_call"] = True + + # get custom llm provider so we can use this for mapping exceptions + if custom_llm_provider is None: + _, custom_llm_provider, _, _ = litellm.get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, api_base=local_vars.get("api_base", None) + ) + + func = partial( + video_list, + after=after, + limit=limit, + order=order, + model=model, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +# fmt: off + +# Overload for when avideo_list=True (returns Coroutine) +@overload +def video_list( + after: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_list: Literal[True], + **kwargs, +) -> Coroutine[Any, Any, Dict[str, Any]]: + ... + + +@overload +def video_list( + after: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_list: Literal[False] = False, + **kwargs, +) -> Dict[str, Any]: + ... + +# fmt: on + + +@client +def video_list( # noqa: PLR0915 + after: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> Union[ + Dict[str, Any], + Coroutine[Any, Any, Dict[str, Any]], +]: + """ + Maps the https://api.openai.com/v1/videos endpoint. + + Currently supports OpenAI + """ + local_vars = locals() + try: + litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore + litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None) + _is_async = kwargs.pop("async_call", False) is True + + # Check for mock response first + mock_response = kwargs.get("mock_response", None) + if mock_response is not None: + if isinstance(mock_response, str): + mock_response = json.loads(mock_response) + return mock_response + + # get llm provider logic + litellm_params = GenericLiteLLMParams(**kwargs) + model, custom_llm_provider, _, _ = get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, + custom_llm_provider=custom_llm_provider, + ) + + # get provider config + video_list_provider_config: Optional[BaseVideoConfig] = ( + ProviderConfigManager.get_provider_video_config( + model=model, + provider=litellm.LlmProviders(custom_llm_provider), + ) + ) + + if video_list_provider_config is None: + raise ValueError(f"video list is not supported for {custom_llm_provider}") + + local_vars.update(kwargs) + # For video list, we need the query parameters + video_list_request_params: Dict = { + "after": after, + "limit": limit, + "order": order, + } + + # Pre Call logging + litellm_logging_obj.update_environment_variables( + model=model, + user=kwargs.get("user"), + optional_params=dict(video_list_request_params), + litellm_params={ + "litellm_call_id": litellm_call_id, + **video_list_request_params, + }, + custom_llm_provider=custom_llm_provider, + ) + + # Set the correct call type for video list + litellm_logging_obj.call_type = CallTypes.video_list.value + + # Call the handler with _is_async flag instead of directly calling the async handler + return base_llm_http_handler.video_list_handler( + after=after, + limit=limit, + order=order, + model=model, + video_list_provider_config=video_list_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=litellm_logging_obj, + extra_headers=extra_headers, + extra_query=extra_query, + timeout=timeout or DEFAULT_REQUEST_TIMEOUT, + _is_async=_is_async, + client=kwargs.get("client"), + ) + + except Exception as e: + raise litellm.exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +##### Video Delete ####################### +@client +async def avideo_delete( + video_id: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> VideoObject: + """ + Asynchronously calls the `video_delete` function with the given arguments and keyword arguments. + + Parameters: + - `video_id` (str): The identifier of the video to delete + - `model` (Optional[str]): The video generation model to use + - `timeout` (int): Request timeout in seconds + - `custom_llm_provider` (Optional[str]): The LLM provider to use + - `extra_headers` (Optional[Dict[str, Any]]): Additional headers + - `extra_query` (Optional[Dict[str, Any]]): Additional query parameters + - `extra_body` (Optional[Dict[str, Any]]): Additional body parameters + - `kwargs` (dict): Additional keyword arguments + + Returns: + - `response` (VideoObject): The response returned by the `video_delete` function. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["async_call"] = True + + # get custom llm provider so we can use this for mapping exceptions + if custom_llm_provider is None: + _, custom_llm_provider, _, _ = litellm.get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, api_base=local_vars.get("api_base", None) + ) + + func = partial( + video_delete, + video_id=video_id, + model=model, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +# fmt: off + +# Overload for when avideo_delete=True (returns Coroutine) +@overload +def video_delete( + video_id: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_delete: Literal[True], + **kwargs, +) -> Coroutine[Any, Any, VideoObject]: + ... + + +@overload +def video_delete( + video_id: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + custom_llm_provider=None, + *, + avideo_delete: Literal[False] = False, + **kwargs, +) -> VideoObject: + ... + +# fmt: on + + +@client +def video_delete( # noqa: PLR0915 + video_id: str, + model: Optional[str] = None, + timeout=600, # default to 10 minutes + custom_llm_provider=None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + **kwargs, +) -> Union[ + VideoObject, + Coroutine[Any, Any, VideoObject], +]: + """ + Maps the https://api.openai.com/v1/videos/{video_id} endpoint. + + Currently supports OpenAI + """ + local_vars = locals() + try: + litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore + litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None) + _is_async = kwargs.pop("async_call", False) is True + + # Check for mock response first + mock_response = kwargs.get("mock_response", None) + if mock_response is not None: + if isinstance(mock_response, str): + mock_response = json.loads(mock_response) + + response = VideoObject(**mock_response) + return response + + # get llm provider logic + litellm_params = GenericLiteLLMParams(**kwargs) + model, custom_llm_provider, _, _ = get_llm_provider( + model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, + custom_llm_provider=custom_llm_provider, + ) + + # get provider config + video_delete_provider_config: Optional[BaseVideoConfig] = ( + ProviderConfigManager.get_provider_video_config( + model=model, + provider=litellm.LlmProviders(custom_llm_provider), + ) + ) + + if video_delete_provider_config is None: + raise ValueError(f"video delete is not supported for {custom_llm_provider}") + + local_vars.update(kwargs) + # For video delete, we need the video_id + video_delete_request_params: Dict = { + "video_id": video_id, + } + + # Pre Call logging + litellm_logging_obj.update_environment_variables( + model=model, + user=kwargs.get("user"), + optional_params=dict(video_delete_request_params), + litellm_params={ + "litellm_call_id": litellm_call_id, + **video_delete_request_params, + }, + custom_llm_provider=custom_llm_provider, + ) + + # Set the correct call type for video delete + litellm_logging_obj.call_type = CallTypes.video_delete.value + + # Call the handler with _is_async flag instead of directly calling the async handler + return base_llm_http_handler.video_delete_handler( + video_id=video_id, + model=model, + video_delete_provider_config=video_delete_provider_config, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=litellm_logging_obj, + extra_headers=extra_headers, + timeout=timeout or DEFAULT_REQUEST_TIMEOUT, + _is_async=_is_async, + client=kwargs.get("client"), + ) + + except Exception as e: + raise litellm.exception_type( + model=model, + custom_llm_provider=custom_llm_provider, + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) \ No newline at end of file From f36a9f8dcc9eb8b70a5c9297e924fc48db571530 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 17 Oct 2025 19:53:23 +0530 Subject: [PATCH 13/17] Fix cost calculation and transformation --- litellm/cost_calculator.py | 13 +- .../llms/base_llm/videos/transformation.py | 155 +++++++++++ litellm/llms/openai/videos/transformation.py | 259 ++++++++++++++++++ 3 files changed, 418 insertions(+), 9 deletions(-) create mode 100644 litellm/llms/base_llm/videos/transformation.py create mode 100644 litellm/llms/openai/videos/transformation.py diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 24db4fb7d099..abb57019f9ab 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -802,14 +802,9 @@ def completion_cost( # noqa: PLR0915 _usage = usage_obj if ResponseAPILoggingUtils._is_response_api_usage(_usage): - # Skip token validation for video generation as it doesn't use token-based pricing - if call_type in [CallTypes.create_video.value, CallTypes.acreate_video.value]: - # For video generation, we'll handle cost calculation separately - pass - else: - _usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( - _usage - ).model_dump() + _usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( + _usage + ).model_dump() # get input/output tokens from completion_response prompt_tokens = _usage.get("prompt_tokens", 0) @@ -1440,7 +1435,7 @@ def default_video_cost_calculator( return output_cost_per_second * duration_seconds # If no cost information found, return 0 - verbose_logger.warning( + verbose_logger.info( f"No cost information found for video model {model}. Please add pricing to model_prices_and_context_window.json" ) return 0.0 diff --git a/litellm/llms/base_llm/videos/transformation.py b/litellm/llms/base_llm/videos/transformation.py new file mode 100644 index 000000000000..060e2e81bd27 --- /dev/null +++ b/litellm/llms/base_llm/videos/transformation.py @@ -0,0 +1,155 @@ +import types +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union + +import httpx +from httpx._types import RequestFiles + +from litellm.types.videos.main import VideoCreateOptionalRequestParams +from litellm.types.responses.main import * +from litellm.types.router import GenericLiteLLMParams + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from litellm.types.videos.main import VideoObject as _VideoObject + + from ..chat.transformation import BaseLLMException as _BaseLLMException + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseLLMException = _BaseLLMException + VideoObject = _VideoObject +else: + LiteLLMLoggingObj = Any + BaseLLMException = Any + VideoObject = Any + + +class BaseVideoConfig(ABC): + def __init__(self): + pass + + @classmethod + def get_config(cls): + return { + k: v + for k, v in cls.__dict__.items() + if not k.startswith("__") + and not k.startswith("_abc") + and not isinstance( + v, + ( + types.FunctionType, + types.BuiltinFunctionType, + classmethod, + staticmethod, + ), + ) + and v is not None + } + + @abstractmethod + def get_supported_openai_params(self, model: str) -> list: + pass + + @abstractmethod + def map_openai_params( + self, + video_create_optional_params: VideoCreateOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + pass + + @abstractmethod + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + return {} + + @abstractmethod + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + OPTIONAL + + Get the complete url for the request + + Some providers need `model` in `api_base` + """ + if api_base is None: + raise ValueError("api_base is required") + return api_base + + @abstractmethod + def transform_video_create_request( + self, + model: str, + prompt: str, + video_create_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[Dict, RequestFiles]: + pass + + @abstractmethod + def transform_video_create_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + pass + + @abstractmethod + def transform_video_content_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> bytes: + pass + + @abstractmethod + def transform_video_remix_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + pass + + @abstractmethod + def transform_video_list_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> Dict[str, Any]: + pass + + @abstractmethod + def transform_video_delete_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + pass + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from ..chat.transformation import BaseLLMException + + raise BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) diff --git a/litellm/llms/openai/videos/transformation.py b/litellm/llms/openai/videos/transformation.py new file mode 100644 index 000000000000..ae46204ba2fd --- /dev/null +++ b/litellm/llms/openai/videos/transformation.py @@ -0,0 +1,259 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, IO +from io import BufferedReader + +import httpx +from httpx._types import RequestFiles + +from litellm.types.videos.main import VideoCreateOptionalRequestParams +from litellm.types.llms.openai import CreateVideoRequest +from litellm.types.videos.main import VideoResponse +from litellm.types.router import GenericLiteLLMParams +from litellm.secret_managers.main import get_secret_str +from litellm.types.videos.main import VideoObject +import litellm + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + + from ...base_llm.videos.transformation import BaseVideoConfig as _BaseVideoConfig + from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseVideoConfig = _BaseVideoConfig + BaseLLMException = _BaseLLMException +else: + LiteLLMLoggingObj = Any + BaseVideoConfig = Any + BaseLLMException = Any + + +class OpenAIVideoConfig(BaseVideoConfig): + """ + Configuration class for OpenAI video generation. + """ + + def __init__(self): + super().__init__() + + def get_supported_openai_params(self, model: str) -> list: + """ + Get the list of supported OpenAI parameters for video generation. + """ + return [ + "model", + "prompt", + "input_reference", + "seconds", + "size", + "user", + "extra_headers", + ] + + def map_openai_params( + self, + video_create_optional_params: VideoCreateOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + """No mapping applied since inputs are in OpenAI spec already""" + return dict(video_create_optional_params) + + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + api_key = ( + api_key + or litellm.api_key + or litellm.openai_key + or get_secret_str("OPENAI_API_KEY") + ) + headers.update( + { + "Authorization": f"Bearer {api_key}", + } + ) + return headers + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for OpenAI video generation. + """ + if api_base is None: + api_base = "https://api.openai.com/v1" + + return f"{api_base.rstrip('/')}/videos" + + def transform_video_create_request( + self, + model: str, + prompt: str, + video_create_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[Dict, RequestFiles]: + """ + Transform the video creation request for OpenAI API. + """ + # Remove model and extra_headers from optional params as they're handled separately + video_create_optional_request_params = { + k: v for k, v in video_create_optional_request_params.items() + if k not in ["model", "extra_headers"] + } + + # Create the request data + video_create_request = CreateVideoRequest( + model=model, + prompt=prompt, + **video_create_optional_request_params + ) + + # Handle file uploads + files_list: List[Tuple[str, Tuple[str, Union[IO[bytes], bytes, str], str]]] = [] + + # Handle input_reference parameter if provided + _input_reference = video_create_optional_request_params.get("input_reference") + if _input_reference is not None: + if isinstance(_input_reference, BufferedReader): + files_list.append( + ("input_reference", (_input_reference.name, _input_reference, "image/png")) + ) + elif isinstance(_input_reference, str): + # Handle file path - open the file + try: + with open(_input_reference, "rb") as f: + files_list.append( + ("input_reference", (f.name, f.read(), "image/png")) + ) + except Exception as e: + raise ValueError(f"Could not open input_reference file {_input_reference}: {e}") + else: + # Handle file-like object + files_list.append( + ("input_reference", ("input_reference.png", _input_reference, "image/png")) + ) + + # Convert to dict for JSON serialization + data = dict(video_create_request) + + return data, files_list + + def transform_video_create_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + """ + Transform the OpenAI video creation response. + """ + response_data = raw_response.json() + + # Transform the response data + + video_obj = VideoObject(**response_data) + + # Create usage object with duration information for cost calculation + # Video generation API doesn't provide usage, so we create one with duration + usage_data = {} + if video_obj: + if hasattr(video_obj, 'seconds') and video_obj.seconds: + try: + usage_data["duration_seconds"] = float(video_obj.seconds) + except (ValueError, TypeError): + pass + # Create the response + video_obj.usage = usage_data + + + return video_obj + + def transform_video_content_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> bytes: + """ + Transform the OpenAI video content download response. + Returns raw video content as bytes. + """ + # For video content download, return the raw content as bytes + return raw_response.content + + def transform_video_remix_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + """ + Transform the OpenAI video remix response. + """ + response_data = raw_response.json() + + # Transform the response data + video_obj = VideoObject(**response_data) + + # Create usage object with duration information for cost calculation + # Video remix API doesn't provide usage, so we create one with duration + usage_data = {} + if video_obj: + if hasattr(video_obj, 'seconds') and video_obj.seconds: + try: + usage_data["duration_seconds"] = float(video_obj.seconds) + except (ValueError, TypeError): + pass + # Create the response + video_obj.usage = usage_data + + return video_obj + + def transform_video_list_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoResponse: + """ + Transform the OpenAI video list response. + """ + response_data = raw_response.json() + response_data = VideoResponse(**response_data) + # The response should already be in the correct format + # Just return it as-is since it matches the expected structure + return response_data + + def transform_video_delete_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + """ + Transform the OpenAI video delete response. + """ + response_data = raw_response.json() + + # Transform the response data + video_obj = VideoObject(**response_data) + + return video_obj + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from ...base_llm.chat.transformation import BaseLLMException + + raise BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) From a6fa7e7c44704f8053a10397d786572ffa373e61 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Sat, 18 Oct 2025 20:27:28 +0530 Subject: [PATCH 14/17] Fixed mypy tests --- litellm/llms/custom_httpx/llm_http_handler.py | 9 +- litellm/llms/openai/videos/transformation.py | 9 +- litellm/videos/main.py | 209 ------------------ 3 files changed, 8 insertions(+), 219 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 5d0e20ee625c..f329c8f5aab0 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -1825,7 +1825,7 @@ def response_api_handler( headers=headers, json=data, timeout=timeout - or response_api_optional_request_params.get("timeout"), + or float(response_api_optional_request_params.get("timeout", 0)), stream=stream, ) if fake_stream is True: @@ -1853,7 +1853,7 @@ def response_api_handler( headers=headers, json=data, timeout=timeout - or response_api_optional_request_params.get("timeout"), + or float(response_api_optional_request_params.get("timeout", 0)), ) except Exception as e: raise self._handle_error( @@ -1946,7 +1946,7 @@ async def async_response_api_handler( headers=headers, json=data, timeout=timeout - or response_api_optional_request_params.get("timeout"), + or float(response_api_optional_request_params.get("timeout", 0)), stream=stream, ) @@ -1976,7 +1976,7 @@ async def async_response_api_handler( headers=headers, json=data, timeout=timeout - or response_api_optional_request_params.get("timeout"), + or float(response_api_optional_request_params.get("timeout", 0)), ) except Exception as e: @@ -4408,7 +4408,6 @@ async def async_video_list_handler( url=api_base, headers=headers, params=params, - timeout=timeout, ) return video_list_provider_config.transform_video_list_response( diff --git a/litellm/llms/openai/videos/transformation.py b/litellm/llms/openai/videos/transformation.py index ae46204ba2fd..ea856cc28e72 100644 --- a/litellm/llms/openai/videos/transformation.py +++ b/litellm/llms/openai/videos/transformation.py @@ -221,15 +221,14 @@ def transform_video_list_response( model: str, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, - ) -> VideoResponse: + ) -> Dict[str, Any]: """ Transform the OpenAI video list response. """ response_data = raw_response.json() - response_data = VideoResponse(**response_data) - # The response should already be in the correct format - # Just return it as-is since it matches the expected structure - return response_data + video_response = VideoResponse(**response_data) + # Convert VideoResponse object to dictionary to match base class return type + return video_response.model_dump() def transform_video_delete_response( self, diff --git a/litellm/videos/main.py b/litellm/videos/main.py index eb6e0cf52bef..2e7b25e9e152 100644 --- a/litellm/videos/main.py +++ b/litellm/videos/main.py @@ -902,212 +902,3 @@ def video_list( # noqa: PLR0915 completion_kwargs=local_vars, extra_kwargs=kwargs, ) - - -##### Video Delete ####################### -@client -async def avideo_delete( - video_id: str, - model: Optional[str] = None, - timeout=600, # default to 10 minutes - custom_llm_provider=None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Optional[Dict[str, Any]] = None, - extra_query: Optional[Dict[str, Any]] = None, - extra_body: Optional[Dict[str, Any]] = None, - **kwargs, -) -> VideoObject: - """ - Asynchronously calls the `video_delete` function with the given arguments and keyword arguments. - - Parameters: - - `video_id` (str): The identifier of the video to delete - - `model` (Optional[str]): The video generation model to use - - `timeout` (int): Request timeout in seconds - - `custom_llm_provider` (Optional[str]): The LLM provider to use - - `extra_headers` (Optional[Dict[str, Any]]): Additional headers - - `extra_query` (Optional[Dict[str, Any]]): Additional query parameters - - `extra_body` (Optional[Dict[str, Any]]): Additional body parameters - - `kwargs` (dict): Additional keyword arguments - - Returns: - - `response` (VideoObject): The response returned by the `video_delete` function. - """ - local_vars = locals() - try: - loop = asyncio.get_event_loop() - kwargs["async_call"] = True - - # get custom llm provider so we can use this for mapping exceptions - if custom_llm_provider is None: - _, custom_llm_provider, _, _ = litellm.get_llm_provider( - model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, api_base=local_vars.get("api_base", None) - ) - - func = partial( - video_delete, - video_id=video_id, - model=model, - timeout=timeout, - custom_llm_provider=custom_llm_provider, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - **kwargs, - ) - - ctx = contextvars.copy_context() - func_with_context = partial(ctx.run, func) - init_response = await loop.run_in_executor(None, func_with_context) - - if asyncio.iscoroutine(init_response): - response = await init_response - else: - response = init_response - - return response - except Exception as e: - raise litellm.exception_type( - model=model, - custom_llm_provider=custom_llm_provider, - original_exception=e, - completion_kwargs=local_vars, - extra_kwargs=kwargs, - ) - - -# fmt: off - -# Overload for when avideo_delete=True (returns Coroutine) -@overload -def video_delete( - video_id: str, - model: Optional[str] = None, - timeout=600, # default to 10 minutes - api_key: Optional[str] = None, - api_base: Optional[str] = None, - api_version: Optional[str] = None, - custom_llm_provider=None, - *, - avideo_delete: Literal[True], - **kwargs, -) -> Coroutine[Any, Any, VideoObject]: - ... - - -@overload -def video_delete( - video_id: str, - model: Optional[str] = None, - timeout=600, # default to 10 minutes - api_key: Optional[str] = None, - api_base: Optional[str] = None, - api_version: Optional[str] = None, - custom_llm_provider=None, - *, - avideo_delete: Literal[False] = False, - **kwargs, -) -> VideoObject: - ... - -# fmt: on - - -@client -def video_delete( # noqa: PLR0915 - video_id: str, - model: Optional[str] = None, - timeout=600, # default to 10 minutes - custom_llm_provider=None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Optional[Dict[str, Any]] = None, - extra_query: Optional[Dict[str, Any]] = None, - extra_body: Optional[Dict[str, Any]] = None, - **kwargs, -) -> Union[ - VideoObject, - Coroutine[Any, Any, VideoObject], -]: - """ - Maps the https://api.openai.com/v1/videos/{video_id} endpoint. - - Currently supports OpenAI - """ - local_vars = locals() - try: - litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore - litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None) - _is_async = kwargs.pop("async_call", False) is True - - # Check for mock response first - mock_response = kwargs.get("mock_response", None) - if mock_response is not None: - if isinstance(mock_response, str): - mock_response = json.loads(mock_response) - - response = VideoObject(**mock_response) - return response - - # get llm provider logic - litellm_params = GenericLiteLLMParams(**kwargs) - model, custom_llm_provider, _, _ = get_llm_provider( - model=model or DEFAULT_VIDEO_ENDPOINT_MODEL, - custom_llm_provider=custom_llm_provider, - ) - - # get provider config - video_delete_provider_config: Optional[BaseVideoConfig] = ( - ProviderConfigManager.get_provider_video_config( - model=model, - provider=litellm.LlmProviders(custom_llm_provider), - ) - ) - - if video_delete_provider_config is None: - raise ValueError(f"video delete is not supported for {custom_llm_provider}") - - local_vars.update(kwargs) - # For video delete, we need the video_id - video_delete_request_params: Dict = { - "video_id": video_id, - } - - # Pre Call logging - litellm_logging_obj.update_environment_variables( - model=model, - user=kwargs.get("user"), - optional_params=dict(video_delete_request_params), - litellm_params={ - "litellm_call_id": litellm_call_id, - **video_delete_request_params, - }, - custom_llm_provider=custom_llm_provider, - ) - - # Set the correct call type for video delete - litellm_logging_obj.call_type = CallTypes.video_delete.value - - # Call the handler with _is_async flag instead of directly calling the async handler - return base_llm_http_handler.video_delete_handler( - video_id=video_id, - model=model, - video_delete_provider_config=video_delete_provider_config, - custom_llm_provider=custom_llm_provider, - litellm_params=litellm_params, - logging_obj=litellm_logging_obj, - extra_headers=extra_headers, - timeout=timeout or DEFAULT_REQUEST_TIMEOUT, - _is_async=_is_async, - client=kwargs.get("client"), - ) - - except Exception as e: - raise litellm.exception_type( - model=model, - custom_llm_provider=custom_llm_provider, - original_exception=e, - completion_kwargs=local_vars, - extra_kwargs=kwargs, - ) \ No newline at end of file From 7ac56be3d697520160282696dfedddcba0a006a8 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 21 Oct 2025 00:04:46 +0530 Subject: [PATCH 15/17] remove not used imports --- litellm/llms/custom_httpx/llm_http_handler.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index f329c8f5aab0..88adaa829772 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -3282,8 +3282,6 @@ def _handle_error( BaseAnthropicMessagesConfig, BaseBatchesConfig, BaseOCRConfig, - BaseVideoGenerationConfig, - "BaseVideoRetrievalConfig", BaseVideoConfig, "BasePassthroughConfig", ], From 12b72d953797b8bd1405599ce43af39712c0bd53 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 22 Oct 2025 17:29:07 +0530 Subject: [PATCH 16/17] fix typed dict for list --- litellm/llms/base_llm/videos/transformation.py | 4 ++-- litellm/llms/openai/videos/transformation.py | 4 ++-- litellm/videos/main.py | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/litellm/llms/base_llm/videos/transformation.py b/litellm/llms/base_llm/videos/transformation.py index 060e2e81bd27..aee655f662b5 100644 --- a/litellm/llms/base_llm/videos/transformation.py +++ b/litellm/llms/base_llm/videos/transformation.py @@ -1,6 +1,6 @@ import types from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, List import httpx from httpx._types import RequestFiles @@ -131,7 +131,7 @@ def transform_video_list_response( model: str, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, - ) -> Dict[str, Any]: + ) -> List[VideoObject]: pass @abstractmethod diff --git a/litellm/llms/openai/videos/transformation.py b/litellm/llms/openai/videos/transformation.py index ea856cc28e72..3c18f81759ad 100644 --- a/litellm/llms/openai/videos/transformation.py +++ b/litellm/llms/openai/videos/transformation.py @@ -221,14 +221,14 @@ def transform_video_list_response( model: str, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, - ) -> Dict[str, Any]: + ) -> List[VideoObject]: """ Transform the OpenAI video list response. """ response_data = raw_response.json() video_response = VideoResponse(**response_data) # Convert VideoResponse object to dictionary to match base class return type - return video_response.model_dump() + return [VideoObject(**video) for video in video_response.data] def transform_video_delete_response( self, diff --git a/litellm/videos/main.py b/litellm/videos/main.py index 2e7b25e9e152..de713aa3e1db 100644 --- a/litellm/videos/main.py +++ b/litellm/videos/main.py @@ -1,7 +1,7 @@ import asyncio import contextvars from functools import partial -from typing import Any, Coroutine, Literal, Optional, Union, overload, Dict +from typing import Any, Coroutine, Literal, Optional, Union, overload, Dict, List import json import litellm @@ -695,7 +695,7 @@ async def avideo_list( extra_query: Optional[Dict[str, Any]] = None, extra_body: Optional[Dict[str, Any]] = None, **kwargs, -) -> Dict[str, Any]: +) -> List[VideoObject]: """ Asynchronously calls the `video_list` function with the given arguments and keyword arguments. @@ -776,7 +776,7 @@ def video_list( *, avideo_list: Literal[True], **kwargs, -) -> Coroutine[Any, Any, Dict[str, Any]]: +) -> Coroutine[Any, Any, List[VideoObject]]: ... @@ -794,7 +794,7 @@ def video_list( *, avideo_list: Literal[False] = False, **kwargs, -) -> Dict[str, Any]: +) -> List[VideoObject]: ... # fmt: on @@ -815,8 +815,8 @@ def video_list( # noqa: PLR0915 extra_body: Optional[Dict[str, Any]] = None, **kwargs, ) -> Union[ - Dict[str, Any], - Coroutine[Any, Any, Dict[str, Any]], + List[VideoObject], + Coroutine[Any, Any, List[VideoObject]], ]: """ Maps the https://api.openai.com/v1/videos endpoint. @@ -834,7 +834,7 @@ def video_list( # noqa: PLR0915 if mock_response is not None: if isinstance(mock_response, str): mock_response = json.loads(mock_response) - return mock_response + return [VideoObject(**item) for item in mock_response] # get llm provider logic litellm_params = GenericLiteLLMParams(**kwargs) From af5b8b0738be31ac129932d409014f321f67ad87 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 22 Oct 2025 17:41:56 +0530 Subject: [PATCH 17/17] fix mypy errors --- litellm/llms/custom_httpx/llm_http_handler.py | 2 +- litellm/llms/openai/responses/transformation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 88adaa829772..5fa09b6e315c 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -4382,7 +4382,7 @@ async def async_video_list_handler( if after is not None: params["after"] = after if limit is not None: - params["limit"] = limit + params["limit"] = str(limit) if order is not None: params["order"] = order diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 1e949e434d3d..79949c34fe0f 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -411,7 +411,7 @@ def transform_list_input_items_request( if include: params["include"] = ",".join(include) if limit is not None: - params["limit"] = limit + params["limit"] = str(limit) if order is not None: params["order"] = order return url, params