fix(videos): re-stamp created video id with resolved model_id so status/content resolve per-model key

This commit is contained in:
Devin AI 2026-07-15 19:20:34 +00:00
parent 5d25e75f3b
commit cbec12becd
3 changed files with 106 additions and 3 deletions

View file

@ -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,

View file

@ -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

View file

@ -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")