From cbec12becd59acf4882e72dc3c836b6593444dd1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:20:34 +0000 Subject: [PATCH] fix(videos): re-stamp created video id with resolved model_id so status/content resolve per-model key --- litellm/proxy/video_endpoints/endpoints.py | 12 +++- litellm/proxy/video_endpoints/utils.py | 41 +++++++++++++- .../proxy/video_endpoints/test_endpoints.py | 56 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/video_endpoints/endpoints.py b/litellm/proxy/video_endpoints/endpoints.py index 5809967cff5..a13e80809c1 100644 --- a/litellm/proxy/video_endpoints/endpoints.py +++ b/litellm/proxy/video_endpoints/endpoints.py @@ -1,6 +1,6 @@ #### Video Endpoints ##### -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, cast import orjson from fastapi import APIRouter, Depends, File, Form, Request, Response, UploadFile @@ -17,7 +17,9 @@ from litellm.proxy.common_utils.openai_endpoint_utils import ( ) from litellm.proxy.image_endpoints.endpoints import batch_to_bytesio from litellm.proxy.video_endpoints.utils import ( + coerce_optional_str, encode_character_id_in_response, + encode_video_id_in_response, extract_model_from_target_model_names, get_custom_provider_from_data, ) @@ -88,7 +90,7 @@ async def video_generation( # Process request using ProxyBaseLLMRequestProcessing processor = ProxyBaseLLMRequestProcessing(data=data) try: - return await processor.base_process_llm_request( + response = await processor.base_process_llm_request( request=request, fastapi_response=fastapi_response, user_api_key_dict=user_api_key_dict, @@ -106,6 +108,12 @@ async def video_generation( user_api_base=user_api_base, version=version, ) + request_data = cast("dict[str, object]", data) + return encode_video_id_in_response( + response=cast(object, response), + fallback_provider=coerce_optional_str(request_data.get("custom_llm_provider")), + fallback_model_id=coerce_optional_str(request_data.get("model")), + ) except Exception as e: raise await processor._handle_llm_api_exception( e=e, diff --git a/litellm/proxy/video_endpoints/utils.py b/litellm/proxy/video_endpoints/utils.py index 412a0e87d88..246483cf51a 100644 --- a/litellm/proxy/video_endpoints/utils.py +++ b/litellm/proxy/video_endpoints/utils.py @@ -2,7 +2,13 @@ from typing import Any, Dict, Optional import orjson -from litellm.types.videos.utils import encode_character_id_with_provider +from litellm.router_utils.add_retry_fallback_headers import get_hidden_params_dict +from litellm.types.videos.main import VideoObject +from litellm.types.videos.utils import ( + decode_video_id_with_provider, + encode_character_id_with_provider, + encode_video_id_with_provider, +) def extract_model_from_target_model_names(target_model_names: Any) -> Optional[str]: @@ -52,3 +58,36 @@ def encode_character_id_in_response(response: Any, custom_llm_provider: str, mod model_id=model_id, ) return response + + +def coerce_optional_str(value: object) -> Optional[str]: + return value if isinstance(value, str) and value else None + + +def encode_video_id_in_response( + response: object, + fallback_provider: Optional[str], + fallback_model_id: Optional[str], +) -> object: + if not isinstance(response, VideoObject) or not response.id: + return response + + hidden_params = get_hidden_params_dict(response) + model_id = coerce_optional_str(hidden_params.get("model_id")) or fallback_model_id + + decoded = decode_video_id_with_provider(response.id) + provider = ( + decoded.get("custom_llm_provider") + or coerce_optional_str(hidden_params.get("custom_llm_provider")) + or fallback_provider + ) + if not provider: + return response + + original_video_id = decoded.get("video_id") or response.id + response.id = encode_video_id_with_provider( + video_id=original_video_id, + provider=provider, + model_id=model_id, + ) + return response diff --git a/tests/test_litellm/proxy/video_endpoints/test_endpoints.py b/tests/test_litellm/proxy/video_endpoints/test_endpoints.py index 40a26fad3c3..12649579652 100644 --- a/tests/test_litellm/proxy/video_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/video_endpoints/test_endpoints.py @@ -44,7 +44,9 @@ from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from litellm.proxy.utils import ProxyLogging from litellm.router import Router +from litellm.types.videos.main import VideoObject from litellm.types.videos.utils import ( + decode_video_id_with_provider, encode_character_id_with_provider, encode_video_id_with_provider, ) @@ -246,6 +248,60 @@ async def test_generation__input_reference_attached(harness): } +def _video_response(video_id: str, hidden_params: Dict[str, Any]) -> VideoObject: + """A VideoObject as returned by the create transform: the id is already + provider-encoded but with an empty model_id (the proxy flow never has the + model at transform time), and the router-set model_id lives in _hidden_params.""" + obj = VideoObject(id=video_id, object="video", status="queued", created_at=0) + obj._hidden_params = hidden_params + return obj + + +@pytest.mark.asyncio +async def test_generation__reencodes_id_with_hidden_params_model_id(harness): + """Regression for #33423: create encodes the video id with an empty model_id, + so GET status/content later cannot resolve the deployment (and thus the + per-model api_key) and fall back to a nonexistent provider key. The endpoint + must re-stamp the returned id with the model_id the router recorded in + _hidden_params so the id round-trips to a resolvable deployment.""" + create_time_id = encode_video_id_with_provider("video_orig123", "azure", "") + # sanity: the id the transform produced really has an empty model_id + assert decode_video_id_with_provider(create_time_id)["model_id"] == "" + + harness.base_process.return_value = _video_response( + create_time_id, + hidden_params={"model_id": VIDEO_MODEL_ID, "custom_llm_provider": "azure"}, + ) + + resp = await call_generation(harness, body={"model": "sora-2", "prompt": "x"}) + + # the returned id now carries the real model_id + provider ... + decoded = decode_video_id_with_provider(resp.id) + assert decoded["model_id"] == VIDEO_MODEL_ID + assert decoded["custom_llm_provider"] == "azure" + assert decoded["video_id"] == "video_orig123" + # ... and is byte-for-byte what a subsequent status/content call decodes and + # resolves to a model name (the router resolver maps VIDEO_MODEL_ID). + assert resp.id == AZURE_VIDEO_ID + assert harness.resolve_model.side_effect(decoded["model_id"]) == "azure-sora" + + +@pytest.mark.asyncio +async def test_generation__reencodes_id_falls_back_to_request_model(harness): + """When the router did not surface a model_id in _hidden_params, the endpoint + falls back to the model from the request body so the id is still stamped.""" + create_time_id = encode_video_id_with_provider("video_orig123", "openai", "") + + harness.base_process.return_value = _video_response(create_time_id, hidden_params={}) + + resp = await call_generation(harness, body={"model": "sora-2", "prompt": "x"}) + + decoded = decode_video_id_with_provider(resp.id) + assert decoded["model_id"] == "sora-2" + assert decoded["custom_llm_provider"] == "openai" + assert decoded["video_id"] == "video_orig123" + + @pytest.mark.asyncio async def test_generation__exception_routed_through_handler(harness): harness.base_process.side_effect = ValueError("provider boom")