[Bugfix] Respect media redirect policy for image references (#6122)
Signed-off-by: HaningZS <resossr0v0@gmail.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
65b3d41b3a
commit
309d9c3ea8
4 files changed
+169
-3
No files matched your search
@@ -167,6 +167,14 @@ transfer options such as `control_path`, `control_guidance`,
|
||||
`num_conditional_frames`, `show_control_condition`, and `show_input`; see the
|
||||
Cosmos3 recipe for complete examples.
|
||||
|
||||
HTTP redirects for `image_reference.image_url` follow vLLM's
|
||||
`VLLM_MEDIA_URL_ALLOW_REDIRECTS` setting. Before starting the server, set it to
|
||||
`1` (the default) to allow redirects or `0` to reject them. A redirect target
|
||||
can differ from the original host, and vLLM-Omni does not yet fully implement
|
||||
upstream vLLM's media URL allowlist protection. Deployments should therefore
|
||||
treat remote media URLs as untrusted and choose this setting as part of their
|
||||
URL access policy.
|
||||
|
||||
### Speech-to-Video
|
||||
|
||||
For models that support audio-driven generation (e.g., Wan2.2-S2V), pass both
|
||||
|
||||
@@ -2,16 +2,122 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for OpenAI-compatible video API encoding helpers."""
|
||||
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
import httpx
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from PIL import Image
|
||||
from vllm import envs
|
||||
|
||||
from vllm_omni.diffusion.postprocess import rife_interpolator
|
||||
from vllm_omni.entrypoints.openai import video_api_utils
|
||||
from vllm_omni.entrypoints.openai.errors import InvalidInputReferenceError
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _png_bytes() -> bytes:
|
||||
buffer = BytesIO()
|
||||
Image.new("RGB", (2, 1), color=(12, 34, 56)).save(buffer, format="PNG")
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def _install_http_transport(monkeypatch, handler, client_kwargs):
|
||||
async_client = httpx.AsyncClient
|
||||
|
||||
def _client_factory(*args, **kwargs):
|
||||
client_kwargs.append(kwargs.copy())
|
||||
return async_client(*args, transport=httpx.MockTransport(handler), **kwargs)
|
||||
|
||||
monkeypatch.setattr(video_api_utils.httpx, "AsyncClient", _client_factory)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decode_image_url_follows_redirects_when_allowed(monkeypatch):
|
||||
requested_paths = []
|
||||
client_kwargs = []
|
||||
|
||||
def _handler(request):
|
||||
requested_paths.append(request.url.path)
|
||||
if request.url.path == "/redirect.png":
|
||||
return httpx.Response(302, headers={"location": "/image.png"})
|
||||
return httpx.Response(200, content=_png_bytes(), headers={"content-type": "image/png"})
|
||||
|
||||
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
|
||||
_install_http_transport(monkeypatch, _handler, client_kwargs)
|
||||
|
||||
image = await video_api_utils.decode_image_url("https://example.com/redirect.png")
|
||||
|
||||
assert image.size == (2, 1)
|
||||
assert image.mode == "RGB"
|
||||
assert requested_paths == ["/redirect.png", "/image.png"]
|
||||
assert client_kwargs == [{"timeout": 60, "follow_redirects": True}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decode_image_url_rejects_redirects_when_disabled(monkeypatch):
|
||||
requested_paths = []
|
||||
client_kwargs = []
|
||||
|
||||
def _handler(request):
|
||||
requested_paths.append(request.url.path)
|
||||
return httpx.Response(302, headers={"location": "/image.png"})
|
||||
|
||||
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", False)
|
||||
_install_http_transport(monkeypatch, _handler, client_kwargs)
|
||||
|
||||
with pytest.raises(InvalidInputReferenceError, match="redirect.*VLLM_MEDIA_URL_ALLOW_REDIRECTS"):
|
||||
await video_api_utils.decode_image_url("https://example.com/redirect.png")
|
||||
|
||||
assert requested_paths == ["/redirect.png"]
|
||||
assert client_kwargs == [{"timeout": 60, "follow_redirects": False}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decode_image_url_reports_http_status(monkeypatch):
|
||||
client_kwargs = []
|
||||
|
||||
def _handler(request):
|
||||
return httpx.Response(404)
|
||||
|
||||
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
|
||||
_install_http_transport(monkeypatch, _handler, client_kwargs)
|
||||
|
||||
with pytest.raises(InvalidInputReferenceError, match="server returned HTTP 404"):
|
||||
await video_api_utils.decode_image_url("https://example.com/missing.png")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decode_image_url_reports_connection_failure(monkeypatch):
|
||||
client_kwargs = []
|
||||
|
||||
def _handler(request):
|
||||
raise httpx.ConnectError("connection refused", request=request)
|
||||
|
||||
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
|
||||
_install_http_transport(monkeypatch, _handler, client_kwargs)
|
||||
|
||||
with pytest.raises(InvalidInputReferenceError, match="failed to download image"):
|
||||
await video_api_utils.decode_image_url("https://example.com/unreachable.png")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decode_image_url_keeps_data_urls_local(monkeypatch):
|
||||
def _unexpected_http_client(*args, **kwargs):
|
||||
pytest.fail("data URLs must not create an HTTP client")
|
||||
|
||||
monkeypatch.setattr(video_api_utils.httpx, "AsyncClient", _unexpected_http_client)
|
||||
encoded_image = base64.b64encode(_png_bytes()).decode()
|
||||
|
||||
image = await video_api_utils.decode_image_url(f"data:image/png;base64,{encoded_image}")
|
||||
|
||||
assert image.size == (2, 1)
|
||||
assert image.mode == "RGB"
|
||||
|
||||
|
||||
def _install_fake_video_mux(monkeypatch, mux_calls):
|
||||
def _fake_mux_video_audio_bytes(frames, audio, fps, audio_sample_rate, video_codec_options=None):
|
||||
mux_calls.append(
|
||||
|
||||
@@ -16,15 +16,17 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import av
|
||||
import httpx
|
||||
import numpy as np
|
||||
import pytest
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
from pytest_mock import MockerFixture
|
||||
from vllm import envs
|
||||
|
||||
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
|
||||
from vllm_omni.entrypoints.openai import api_server
|
||||
from vllm_omni.entrypoints.openai import api_server, video_api_utils
|
||||
from vllm_omni.entrypoints.openai.api_server import router
|
||||
from vllm_omni.entrypoints.openai.protocol.videos import (
|
||||
VideoGenerationRequest,
|
||||
@@ -599,6 +601,45 @@ def test_i2v_video_generation_with_image_reference_form(test_client, mocker: Moc
|
||||
assert input_image.size == (40, 24)
|
||||
|
||||
|
||||
def test_i2v_video_generation_follows_allowed_image_redirect(test_client, mocker: MockerFixture, monkeypatch):
|
||||
requested_paths = []
|
||||
async_client = httpx.AsyncClient
|
||||
|
||||
def _handler(request):
|
||||
requested_paths.append(request.url.path)
|
||||
if request.url.path == "/redirect.png":
|
||||
return httpx.Response(302, headers={"location": "/image.png"})
|
||||
return httpx.Response(200, content=_make_test_image_bytes((40, 24)))
|
||||
|
||||
def _client_factory(*args, **kwargs):
|
||||
return async_client(*args, transport=httpx.MockTransport(_handler), **kwargs)
|
||||
|
||||
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
|
||||
monkeypatch.setattr(video_api_utils.httpx, "AsyncClient", _client_factory)
|
||||
mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
return_value=b"fake-video",
|
||||
)
|
||||
|
||||
response = test_client.post(
|
||||
"/v1/videos",
|
||||
data={
|
||||
"prompt": "A fox running through snow.",
|
||||
"image_reference": json.dumps({"image_url": "https://example.com/redirect.png"}),
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
video_id = response.json()["id"]
|
||||
_wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
|
||||
assert requested_paths == ["/redirect.png", "/image.png"]
|
||||
|
||||
engine = test_client.app.state.openai_serving_video._engine_client
|
||||
input_image = engine.captured_prompt["multi_modal_data"]["image"]
|
||||
assert isinstance(input_image, Image.Image)
|
||||
assert input_image.size == (40, 24)
|
||||
|
||||
|
||||
def test_v2v_video_generation_form(test_client, mocker: MockerFixture):
|
||||
video_bytes = _make_test_video_bytes((32, 24), num_frames=3)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import httpx
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from vllm import envs
|
||||
from vllm.multimodal.video import (
|
||||
VIDEO_LOADER_REGISTRY,
|
||||
VideoBackend,
|
||||
@@ -172,11 +173,21 @@ async def decode_image_url(image_url: str) -> Image.Image:
|
||||
return _decode_base64_image(image_url, source="image_reference.image_url")
|
||||
|
||||
if image_url.startswith(("http://", "https://")):
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
allow_redirects = envs.VLLM_MEDIA_URL_ALLOW_REDIRECTS
|
||||
async with httpx.AsyncClient(timeout=60, follow_redirects=allow_redirects) as client:
|
||||
try:
|
||||
response = await client.get(image_url)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as exc:
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response.has_redirect_location and not allow_redirects:
|
||||
raise InvalidInputReferenceError(
|
||||
"Invalid image_reference.image_url: redirect response was rejected because "
|
||||
"VLLM_MEDIA_URL_ALLOW_REDIRECTS is disabled."
|
||||
) from exc
|
||||
raise InvalidInputReferenceError(
|
||||
f"Invalid image_reference.image_url: server returned HTTP {exc.response.status_code}."
|
||||
) from exc
|
||||
except httpx.RequestError as exc:
|
||||
raise InvalidInputReferenceError(
|
||||
"Invalid image_reference.image_url: failed to download image."
|
||||
) from exc
|
||||
|
||||
Reference in new issue
Block a user