mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(proxy): parse form-encoded video edit/extension bodies after auth
Fixes #36487 video_edit, video_extension, and video_remix called request.body() after user_api_key_auth had already parsed multipart/form bodies via _read_request_body(), causing RuntimeError Stream consumed and 500s for OpenAI SDK clients. Use _read_request_body consistently and normalize bare-string or JSON-string video references from form fields into video_id.
This commit is contained in:
parent
1a8cd8a078
commit
e3da917e67
4 changed files with 100 additions and 20 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from typing import Any, Final
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, Form, Request, Response, UploadFile
|
||||
from fastapi.responses import ORJSONResponse
|
||||
|
||||
|
|
@ -20,6 +19,7 @@ from litellm.proxy.video_endpoints.utils import (
|
|||
encode_character_id_in_response,
|
||||
extract_model_from_target_model_names,
|
||||
get_custom_provider_from_data,
|
||||
pop_video_reference_to_video_id,
|
||||
)
|
||||
from litellm.types.videos.utils import (
|
||||
decode_character_id_with_provider,
|
||||
|
|
@ -451,9 +451,7 @@ async def video_remix(
|
|||
version,
|
||||
)
|
||||
|
||||
# Read request body
|
||||
body: Final = await request.body()
|
||||
data: Final = orjson.loads(body)
|
||||
data: Final = await _read_request_body(request=request)
|
||||
data["video_id"] = video_id
|
||||
|
||||
decoded: Final = decode_video_id_with_provider(video_id)
|
||||
|
|
@ -760,15 +758,10 @@ async def video_edit(
|
|||
version,
|
||||
)
|
||||
|
||||
body: Final = await request.body()
|
||||
data: Final = orjson.loads(body)
|
||||
data: Final = await _read_request_body(request=request)
|
||||
pop_video_reference_to_video_id(data)
|
||||
|
||||
# Extract video_id from nested video object
|
||||
video_ref: Final = data.pop("video", {})
|
||||
video_id: Final = video_ref.get("id", "") if isinstance(video_ref, dict) else ""
|
||||
data["video_id"] = video_id
|
||||
|
||||
decoded: Final = decode_video_id_with_provider(video_id)
|
||||
decoded: Final = decode_video_id_with_provider(data["video_id"])
|
||||
provider_from_id: Final = decoded.get("custom_llm_provider")
|
||||
model_id_from_decoded: Final = decoded.get("model_id")
|
||||
|
||||
|
|
@ -860,15 +853,10 @@ async def video_extension(
|
|||
version,
|
||||
)
|
||||
|
||||
body: Final = await request.body()
|
||||
data: Final = orjson.loads(body)
|
||||
data: Final = await _read_request_body(request=request)
|
||||
pop_video_reference_to_video_id(data)
|
||||
|
||||
# Extract video_id from nested video object
|
||||
video_ref: Final = data.pop("video", {})
|
||||
video_id: Final = video_ref.get("id", "") if isinstance(video_ref, dict) else ""
|
||||
data["video_id"] = video_id
|
||||
|
||||
decoded: Final = decode_video_id_with_provider(video_id)
|
||||
decoded: Final = decode_video_id_with_provider(data["video_id"])
|
||||
provider_from_id: Final = decoded.get("custom_llm_provider")
|
||||
model_id_from_decoded: Final = decoded.get("model_id")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,27 @@ def extract_model_from_target_model_names(target_model_names: Any) -> str | None
|
|||
return target_model_names[0] if target_model_names else None
|
||||
|
||||
|
||||
def pop_video_reference_to_video_id(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Normalize OpenAI video edit/extension payloads into ``video_id``.
|
||||
|
||||
JSON bodies use ``video: {"id": ...}``. Multipart and form-urlencoded bodies
|
||||
may send a bare id string or a JSON-encoded reference object as a string field.
|
||||
"""
|
||||
video_ref: Final = data.pop("video", {})
|
||||
if isinstance(video_ref, dict):
|
||||
video_id: Final = video_ref.get("id", "")
|
||||
elif isinstance(video_ref, str):
|
||||
try:
|
||||
parsed_ref: Final = orjson.loads(video_ref)
|
||||
except orjson.JSONDecodeError:
|
||||
parsed_ref = None
|
||||
video_id = parsed_ref.get("id", "") if isinstance(parsed_ref, dict) else video_ref
|
||||
else:
|
||||
video_id = ""
|
||||
data["video_id"] = video_id
|
||||
|
||||
|
||||
def get_custom_provider_from_data(data: dict[str, Any]) -> str | None:
|
||||
custom_llm_provider: Final = data.get("custom_llm_provider")
|
||||
if custom_llm_provider:
|
||||
|
|
|
|||
|
|
@ -375,6 +375,7 @@ async def test_content__model_encoded_id(harness):
|
|||
async def call_edit(
|
||||
harness: Harness, *, body: Dict[str, Any], headers=None, query=None
|
||||
):
|
||||
harness.read_body.return_value = dict(body)
|
||||
return await endpoints.video_edit(
|
||||
request=FakeRequest(headers=headers, query=query, raw_body=orjson.dumps(body)),
|
||||
fastapi_response=Response(),
|
||||
|
|
@ -431,6 +432,27 @@ async def test_edit__missing_video_object_defaults_to_openai(harness):
|
|||
assert "video" not in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit__bare_string_video_id_from_form_field(harness):
|
||||
await call_edit(harness, body={"prompt": "brighter", "video": "video_plain"})
|
||||
|
||||
assert harness.processor_data() == {
|
||||
"prompt": "brighter",
|
||||
"video_id": "video_plain",
|
||||
"custom_llm_provider": "openai",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit__json_string_video_reference_from_form_field(harness):
|
||||
await call_edit(
|
||||
harness,
|
||||
body={"prompt": "brighter", "video": orjson.dumps({"id": "video_plain"}).decode()},
|
||||
)
|
||||
|
||||
assert harness.processor_data()["video_id"] == "video_plain"
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# GET /v1/videos - video_list #
|
||||
# =========================================================================== #
|
||||
|
|
@ -474,6 +496,7 @@ async def test_list__provider_from_header(harness):
|
|||
async def call_remix(
|
||||
harness: Harness, video_id: str, *, body, headers=None, query=None
|
||||
):
|
||||
harness.read_body.return_value = dict(body)
|
||||
return await endpoints.video_remix(
|
||||
video_id=video_id,
|
||||
request=FakeRequest(headers=headers, query=query, raw_body=orjson.dumps(body)),
|
||||
|
|
@ -632,6 +655,7 @@ async def test_get_character__plain_id_defaults_openai_no_encode(harness):
|
|||
|
||||
|
||||
async def call_extension(harness: Harness, *, body, headers=None, query=None):
|
||||
harness.read_body.return_value = dict(body)
|
||||
return await endpoints.video_extension(
|
||||
request=FakeRequest(headers=headers, query=query, raw_body=orjson.dumps(body)),
|
||||
fastapi_response=Response(),
|
||||
|
|
|
|||
|
|
@ -2321,6 +2321,53 @@ def test_edit_and_extension_support_custom_provider_from_extra_body(
|
|||
assert captured_data["custom_llm_provider"] == "vertex_ai"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endpoint", ["/v1/videos/edits", "/v1/videos/extensions"])
|
||||
def test_edit_and_extension_accept_form_encoded_after_auth_reads_body(
|
||||
video_proxy_test_client, endpoint
|
||||
):
|
||||
from fastapi import Request
|
||||
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
|
||||
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
|
||||
captured_data = {}
|
||||
|
||||
async def _mock_base_process(self, **kwargs):
|
||||
captured_data.update(self.data)
|
||||
return {
|
||||
"id": "video_resp_123",
|
||||
"object": "video",
|
||||
"status": "queued",
|
||||
"created_at": 1712697600,
|
||||
}
|
||||
|
||||
async def auth_that_reads_body_first(request: Request):
|
||||
await _read_request_body(request=request)
|
||||
return MagicMock()
|
||||
|
||||
app = video_proxy_test_client.app
|
||||
app.dependency_overrides[user_api_key_auth] = auth_that_reads_body_first
|
||||
|
||||
with patch.object(
|
||||
ProxyBaseLLMRequestProcessing,
|
||||
"base_process_llm_request",
|
||||
new=_mock_base_process,
|
||||
):
|
||||
response = video_proxy_test_client.post(
|
||||
endpoint,
|
||||
headers={"Authorization": "Bearer sk-1234"},
|
||||
data={
|
||||
"model": "my-video-model",
|
||||
"prompt": "brighter",
|
||||
"video": "video_123",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert captured_data["video_id"] == "video_123"
|
||||
assert captured_data["prompt"] == "brighter"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endpoint", ["/v1/videos/edits", "/v1/videos/extensions"])
|
||||
def test_edit_and_extension_route_with_encoded_video_ids(
|
||||
video_proxy_test_client, endpoint
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue