mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(videos): re-stamp created video id with resolved model_id so status/content resolve per-model key
This commit is contained in:
parent
5d25e75f3b
commit
cbec12becd
3 changed files with 106 additions and 3 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue