diff --git a/examples/online_serving/helios/README.md b/examples/online_serving/helios/README.md new file mode 100644 index 000000000..5e13df19d --- /dev/null +++ b/examples/online_serving/helios/README.md @@ -0,0 +1,175 @@ +# Helios Video Generation via Online API + +This example demonstrates how to use the `/v1/videos` API with Helios models using the generic `extra_params` field. + +## Overview + +The `/v1/videos` API now supports model-specific parameters through the `extra_params` field, which accepts a JSON object containing any model-specific configuration. This allows supporting new models like Helios without modifying the API. + +## Helios Model Variants + +- **Helios-Base**: Basic T2V/I2V/V2V generation (Stage 1 only) +- **Helios-Mid**: Advanced generation with CFG-Zero* (Stage 2) +- **Helios-Distilled**: Fast generation with DMD (Stage 2) + +## API Usage Examples + +### 1. T2V (Text-to-Video) - Helios-Base + +Basic text-to-video generation: + +```bash +curl -X POST http://localhost:8000/v1/videos \ + -F "prompt=A serene lakeside sunrise with mist over the water." \ + -F "model=BestWishYsh/Helios-Base" \ + -F "width=640" \ + -F "height=384" \ + -F "num_frames=99" \ + -F "num_inference_steps=50" \ + -F "guidance_scale=5.0" \ + -F "seed=42" +``` + +### 2. I2V (Image-to-Video) - Helios-Base + +Generate video from an input image: + +```bash +curl -X POST http://localhost:8000/v1/videos \ + -F "prompt=The lake water gently ripples as morning mist rises." \ + -F "model=BestWishYsh/Helios-Base" \ + -F "input_reference=@/path/to/image.jpg" \ + -F "width=640" \ + -F "height=384" \ + -F "num_frames=99" \ + -F "guidance_scale=5.0" +``` + +### 3. Helios-Mid with Stage 2 + CFG-Zero* + +Advanced generation with pyramid multi-stage denoising: + +```bash +curl -X POST http://localhost:8000/v1/videos \ + -F "prompt=A serene lakeside sunrise with mist over the water." \ + -F "model=BestWishYsh/Helios-Mid" \ + -F "width=640" \ + -F "height=384" \ + -F "guidance_scale=5.0" \ + -F 'extra_params={ + "is_enable_stage2": true, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [20, 20, 20], + "use_cfg_zero_star": true, + "use_zero_init": true, + "zero_steps": 1 + }' +``` + +### 4. Helios-Distilled with DMD + +Fast generation with Distribution Matching Distillation: + +```bash +curl -X POST http://localhost:8000/v1/videos \ + -F "prompt=A serene lakeside sunrise with mist over the water." \ + -F "model=BestWishYsh/Helios-Distilled" \ + -F "width=640" \ + -F "height=384" \ + -F "guidance_scale=1.0" \ + -F 'extra_params={ + "is_enable_stage2": true, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [2, 2, 2], + "is_amplify_first_chunk": true + }' +``` + +## Model-Specific Parameters + +The `extra_params` field accepts a JSON object with model-specific parameters. For Helios models, supported parameters include: + +### Stage 2 Parameters +- `is_enable_stage2` (bool): Enable pyramid multi-stage denoising +- `pyramid_num_stages` (int): Number of pyramid stages (default: 3) +- `pyramid_num_inference_steps_list` (array): Steps per stage, e.g., `[20, 20, 20]` + +### CFG Zero Star Parameters (Helios-Mid) +- `use_cfg_zero_star` (bool): Enable CFG Zero Star guidance +- `use_zero_init` (bool): Use zero initialization for first steps +- `zero_steps` (int): Number of initial zero prediction steps (default: 1) + +### DMD Parameters (Helios-Distilled) +- `is_amplify_first_chunk` (bool): Enable DMD amplification for first chunk + +### Video Input (V2V mode) +For video-to-video generation, upload a video file via `input_reference`: + +```bash +curl -X POST http://localhost:8000/v1/videos \ + -F "prompt=Transform the video into a watercolor painting style." \ + -F "model=BestWishYsh/Helios-Base" \ + -F "input_reference=@/path/to/video.mp4" \ + -F "guidance_scale=5.0" +``` + +## Python Example + +```python +import requests +import json + +url = "http://localhost:8000/v1/videos" + +# Helios-Mid with Stage 2 +data = { + "prompt": "A serene lakeside sunrise with mist over the water.", + "model": "BestWishYsh/Helios-Mid", + "width": 640, + "height": 384, + "guidance_scale": 5.0, + "extra_params": json.dumps({ + "is_enable_stage2": True, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [20, 20, 20], + "use_cfg_zero_star": True, + "use_zero_init": True, + "zero_steps": 1 + }) +} + +response = requests.post(url, data=data) +video_job = response.json() +print(f"Video job created: {video_job['id']}") + +# Poll for completion +import time +while True: + status_response = requests.get(f"{url}/{video_job['id']}") + status = status_response.json() + + if status['status'] == 'completed': + print(f"Video generated: {status['file_name']}") + break + elif status['status'] == 'failed': + print(f"Generation failed: {status.get('error')}") + break + + print(f"Progress: {status['progress']}%") + time.sleep(2) +``` + +## Benefits of This Approach + +1. **No API Changes**: New models can be supported without modifying the API +2. **Backward Compatible**: Existing clients continue to work +3. **Flexible**: Any model-specific parameter can be passed +4. **Type-Safe**: Parameters are validated at the model level +5. **Future-Proof**: Supports models that don't exist yet + +## Notes + +- The `extra_params` field must be a valid JSON object +- Parameters are passed directly to the model's `extra_args` +- Invalid parameters will be caught by the model implementation +- Tensor inputs (images/videos) should use `input_reference`, not `extra_params` diff --git a/examples/online_serving/helios/helios_client.py b/examples/online_serving/helios/helios_client.py new file mode 100644 index 000000000..d50249966 --- /dev/null +++ b/examples/online_serving/helios/helios_client.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +""" +Helios video generation client example using the /v1/videos API. + +This example demonstrates how to use the generic extra_params field +to pass Helios-specific parameters without modifying the API. +""" + +import argparse +import json +import time +from pathlib import Path + +import requests + + +def create_video_job( + api_url: str, + prompt: str, + model: str, + width: int = 640, + height: int = 384, + num_frames: int = 99, + guidance_scale: float = 5.0, + seed: int = 42, + extra_params: dict | None = None, + input_image: Path | None = None, +) -> dict: + """Create a video generation job.""" + data = { + "prompt": prompt, + "model": model, + "width": width, + "height": height, + "num_frames": num_frames, + "guidance_scale": guidance_scale, + "seed": seed, + } + + files = {} + if input_image: + files["input_reference"] = open(input_image, "rb") + + if extra_params: + data["extra_params"] = json.dumps(extra_params) + + response = requests.post(f"{api_url}/v1/videos", data=data, files=files) + response.raise_for_status() + + if files: + files["input_reference"].close() + + return response.json() + + +def poll_video_status(api_url: str, video_id: str, poll_interval: int = 2) -> dict: + """Poll video generation status until completion.""" + print(f"Polling video job: {video_id}") + + while True: + response = requests.get(f"{api_url}/v1/videos/{video_id}") + response.raise_for_status() + status_data = response.json() + + status = status_data["status"] + progress = status_data.get("progress", 0) + + print(f"Status: {status}, Progress: {progress}%") + + if status == "completed": + print("Video generation completed!") + return status_data + elif status == "failed": + error = status_data.get("error", {}) + raise RuntimeError(f"Video generation failed: {error}") + + time.sleep(poll_interval) + + +def main(): + parser = argparse.ArgumentParser(description="Helios video generation client") + parser.add_argument( + "--api-url", + default="http://localhost:8000", + help="API server URL", + ) + parser.add_argument( + "--model", + default="BestWishYsh/Helios-Base", + help="Model name (Helios-Base, Helios-Mid, Helios-Distilled)", + ) + parser.add_argument( + "--prompt", + default="A serene lakeside sunrise with mist over the water.", + help="Text prompt", + ) + parser.add_argument( + "--width", + type=int, + default=640, + help="Video width", + ) + parser.add_argument( + "--height", + type=int, + default=384, + help="Video height", + ) + parser.add_argument( + "--num-frames", + type=int, + default=99, + help="Number of frames", + ) + parser.add_argument( + "--guidance-scale", + type=float, + default=5.0, + help="Guidance scale", + ) + parser.add_argument( + "--seed", + type=int, + default=42, + help="Random seed", + ) + parser.add_argument( + "--input-image", + type=Path, + help="Input image for I2V mode", + ) + parser.add_argument( + "--preset", + choices=["base", "mid-stage2", "distilled"], + default="base", + help="Helios preset configuration", + ) + + args = parser.parse_args() + + # Define preset configurations + presets = { + "base": None, # No extra params for base model + "mid-stage2": { + "is_enable_stage2": True, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [20, 20, 20], + "use_cfg_zero_star": True, + "use_zero_init": True, + "zero_steps": 1, + }, + "distilled": { + "is_enable_stage2": True, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [2, 2, 2], + "is_amplify_first_chunk": True, + }, + } + + extra_params = presets[args.preset] + + print("=" * 50) + print("Helios Video Generation") + print("=" * 50) + print(f"API URL: {args.api_url}") + print(f"Model: {args.model}") + print(f"Preset: {args.preset}") + print(f"Prompt: {args.prompt}") + print(f"Size: {args.width}x{args.height}") + print(f"Frames: {args.num_frames}") + print(f"Guidance Scale: {args.guidance_scale}") + if extra_params: + print(f"Extra Params: {json.dumps(extra_params, indent=2)}") + print() + + # Create video job + print("Creating video generation job...") + job = create_video_job( + api_url=args.api_url, + prompt=args.prompt, + model=args.model, + width=args.width, + height=args.height, + num_frames=args.num_frames, + guidance_scale=args.guidance_scale, + seed=args.seed, + extra_params=extra_params, + input_image=args.input_image, + ) + + video_id = job["id"] + print(f"Video job created: {video_id}") + print() + + # Poll for completion + result = poll_video_status(args.api_url, video_id) + + print() + print("=" * 50) + print("Generation Complete") + print("=" * 50) + print(f"Video ID: {result['id']}") + print(f"File: {result.get('file_name', 'N/A')}") + print(f"Inference Time: {result.get('inference_time_s', 'N/A')}s") + print() + + +if __name__ == "__main__": + main() diff --git a/examples/online_serving/helios/run_helios_distilled.sh b/examples/online_serving/helios/run_helios_distilled.sh new file mode 100644 index 000000000..15b6d8c30 --- /dev/null +++ b/examples/online_serving/helios/run_helios_distilled.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +# Helios-Distilled with DMD Example +# Fast generation with Distribution Matching Distillation + +API_URL="${API_URL:-http://localhost:8000}" +MODEL="${MODEL:-BestWishYsh/Helios-Distilled}" +PROMPT="${PROMPT:-A serene lakeside sunrise with mist over the water.}" + +echo "===================================" +echo "Helios-Distilled with DMD" +echo "===================================" +echo "API URL: $API_URL" +echo "Model: $MODEL" +echo "Prompt: $PROMPT" +echo "" + +# Model-specific parameters for Helios-Distilled +extra_params='{ + "is_enable_stage2": true, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [2, 2, 2], + "is_amplify_first_chunk": true +}' + +echo "Model extra params: $extra_params" +echo "" + +# Create video generation job +echo "Creating video generation job..." +RESPONSE=$(curl -s -X POST "$API_URL/v1/videos" \ + -F "prompt=$PROMPT" \ + -F "model=$MODEL" \ + -F "width=640" \ + -F "height=384" \ + -F "guidance_scale=1.0" \ + -F "seed=42" \ + -F "extra_params=$extra_params") + +echo "Response: $RESPONSE" +echo "" + +# Extract video ID +VIDEO_ID=$(echo "$RESPONSE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + +if [ -z "$VIDEO_ID" ]; then + echo "Error: Failed to create video job" + exit 1 +fi + +echo "Video job created: $VIDEO_ID" +echo "" + +# Poll for completion +echo "Polling for completion..." +while true; do + STATUS_RESPONSE=$(curl -s "$API_URL/v1/videos/$VIDEO_ID") + STATUS=$(echo "$STATUS_RESPONSE" | grep -o '"status":"[^"]*"' | cut -d'"' -f4) + PROGRESS=$(echo "$STATUS_RESPONSE" | grep -o '"progress":[0-9]*' | cut -d':' -f2) + + echo "Status: $STATUS, Progress: $PROGRESS%" + + if [ "$STATUS" = "completed" ]; then + echo "" + echo "Video generation completed!" + echo "Full response:" + echo "$STATUS_RESPONSE" | jq '.' + break + elif [ "$STATUS" = "failed" ]; then + echo "" + echo "Video generation failed!" + echo "$STATUS_RESPONSE" | jq '.' + exit 1 + fi + + sleep 2 +done diff --git a/examples/online_serving/helios/run_helios_mid_stage2.sh b/examples/online_serving/helios/run_helios_mid_stage2.sh new file mode 100644 index 000000000..d5da9b22c --- /dev/null +++ b/examples/online_serving/helios/run_helios_mid_stage2.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +# Helios-Mid with Stage 2 + CFG-Zero* Example +# This demonstrates advanced generation with pyramid multi-stage denoising + +API_URL="${API_URL:-http://localhost:8000}" +MODEL="${MODEL:-BestWishYsh/Helios-Mid}" +PROMPT="${PROMPT:-A serene lakeside sunrise with mist over the water.}" + +echo "===================================" +echo "Helios-Mid Stage 2 + CFG-Zero*" +echo "===================================" +echo "API URL: $API_URL" +echo "Model: $MODEL" +echo "Prompt: $PROMPT" +echo "" + +# Model-specific parameters for Helios-Mid +extra_params='{ + "is_enable_stage2": true, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [20, 20, 20], + "use_cfg_zero_star": true, + "use_zero_init": true, + "zero_steps": 1 +}' + +echo "Model extra params: $extra_params" +echo "" + +# Create video generation job +echo "Creating video generation job..." +RESPONSE=$(curl -s -X POST "$API_URL/v1/videos" \ + -F "prompt=$PROMPT" \ + -F "model=$MODEL" \ + -F "width=640" \ + -F "height=384" \ + -F "guidance_scale=5.0" \ + -F "seed=42" \ + -F "extra_params=$extra_params") + +echo "Response: $RESPONSE" +echo "" + +# Extract video ID +VIDEO_ID=$(echo "$RESPONSE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + +if [ -z "$VIDEO_ID" ]; then + echo "Error: Failed to create video job" + exit 1 +fi + +echo "Video job created: $VIDEO_ID" +echo "" + +# Poll for completion +echo "Polling for completion..." +while true; do + STATUS_RESPONSE=$(curl -s "$API_URL/v1/videos/$VIDEO_ID") + STATUS=$(echo "$STATUS_RESPONSE" | grep -o '"status":"[^"]*"' | cut -d'"' -f4) + PROGRESS=$(echo "$STATUS_RESPONSE" | grep -o '"progress":[0-9]*' | cut -d':' -f2) + + echo "Status: $STATUS, Progress: $PROGRESS%" + + if [ "$STATUS" = "completed" ]; then + echo "" + echo "Video generation completed!" + echo "Full response:" + echo "$STATUS_RESPONSE" | jq '.' + break + elif [ "$STATUS" = "failed" ]; then + echo "" + echo "Video generation failed!" + echo "$STATUS_RESPONSE" | jq '.' + exit 1 + fi + + sleep 2 +done diff --git a/examples/online_serving/helios/run_helios_t2v.sh b/examples/online_serving/helios/run_helios_t2v.sh new file mode 100644 index 000000000..e920fc872 --- /dev/null +++ b/examples/online_serving/helios/run_helios_t2v.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +# Helios Text-to-Video Generation Example +# This script demonstrates how to use the /v1/videos API with Helios models + +API_URL="${API_URL:-http://localhost:8000}" +MODEL="${MODEL:-BestWishYsh/Helios-Base}" +PROMPT="${PROMPT:-A serene lakeside sunrise with mist over the water.}" + +echo "===================================" +echo "Helios T2V Generation" +echo "===================================" +echo "API URL: $API_URL" +echo "Model: $MODEL" +echo "Prompt: $PROMPT" +echo "" + +# Create video generation job +echo "Creating video generation job..." +RESPONSE=$(curl -s -X POST "$API_URL/v1/videos" \ + -F "prompt=$PROMPT" \ + -F "model=$MODEL" \ + -F "width=640" \ + -F "height=384" \ + -F "num_frames=99" \ + -F "num_inference_steps=50" \ + -F "guidance_scale=5.0" \ + -F "seed=42") + +echo "Response: $RESPONSE" +echo "" + +# Extract video ID +VIDEO_ID=$(echo "$RESPONSE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + +if [ -z "$VIDEO_ID" ]; then + echo "Error: Failed to create video job" + exit 1 +fi + +echo "Video job created: $VIDEO_ID" +echo "" + +# Poll for completion +echo "Polling for completion..." +while true; do + STATUS_RESPONSE=$(curl -s "$API_URL/v1/videos/$VIDEO_ID") + STATUS=$(echo "$STATUS_RESPONSE" | grep -o '"status":"[^"]*"' | cut -d'"' -f4) + PROGRESS=$(echo "$STATUS_RESPONSE" | grep -o '"progress":[0-9]*' | cut -d':' -f2) + + echo "Status: $STATUS, Progress: $PROGRESS%" + + if [ "$STATUS" = "completed" ]; then + echo "" + echo "Video generation completed!" + echo "Full response:" + echo "$STATUS_RESPONSE" | jq '.' + break + elif [ "$STATUS" = "failed" ]; then + echo "" + echo "Video generation failed!" + echo "$STATUS_RESPONSE" | jq '.' + exit 1 + fi + + sleep 2 +done diff --git a/tests/entrypoints/openai_api/test_video_server.py b/tests/entrypoints/openai_api/test_video_server.py index f8a8a1e4b..f8d68d95a 100644 --- a/tests/entrypoints/openai_api/test_video_server.py +++ b/tests/entrypoints/openai_api/test_video_server.py @@ -638,3 +638,100 @@ def test_video_response_file_extension_is_robust(): media_type="application/x-custom-video", ) _ = unknown.file_extension + + +def test_extra_params_merged_into_extra_args(test_client, mocker: MockerFixture): + """extra_params JSON object is merged into sampling_params.extra_args.""" + mocker.patch( + "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", + return_value="Zg==", + ) + extra_params = { + "is_enable_stage2": True, + "pyramid_num_stages": 3, + "pyramid_num_inference_steps_list": [20, 20, 20], + "use_cfg_zero_star": True, + } + response = test_client.post( + "/v1/videos", + data={ + "prompt": "A rocket launching.", + "extra_params": json.dumps(extra_params), + }, + ) + + assert response.status_code == 200 + video_id = response.json()["id"] + _wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value) + engine = test_client.app.state.openai_serving_video._engine_client + captured = engine.captured_sampling_params_list[0] + assert captured.extra_args["is_enable_stage2"] is True + assert captured.extra_args["pyramid_num_stages"] == 3 + assert captured.extra_args["pyramid_num_inference_steps_list"] == [20, 20, 20] + assert captured.extra_args["use_cfg_zero_star"] is True + + +def test_extra_params_none_by_default(test_client, mocker: MockerFixture): + """When extra_params is omitted, extra_args stays empty.""" + mocker.patch( + "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", + return_value="Zg==", + ) + response = test_client.post( + "/v1/videos", + data={"prompt": "A calm river."}, + ) + + assert response.status_code == 200 + video_id = response.json()["id"] + _wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value) + engine = test_client.app.state.openai_serving_video._engine_client + captured = engine.captured_sampling_params_list[0] + assert "is_enable_stage2" not in captured.extra_args + + +def test_extra_params_invalid_json(test_client): + """Malformed JSON for extra_params returns 400.""" + response = test_client.post( + "/v1/videos", + data={ + "prompt": "A forest.", + "extra_params": "{not valid json}", + }, + ) + assert response.status_code == 400 + + """extra_params must be a JSON object, not an array.""" + response = test_client.post( + "/v1/videos", + data={ + "prompt": "A desert.", + "extra_params": json.dumps([1, 2, 3]), + }, + ) + assert response.status_code == 400 + + +def test_extra_params_merged_with_existing_extra_args(test_client, mocker: MockerFixture): + """extra_params is merged on top of existing extra_args (e.g. flow_shift).""" + mocker.patch( + "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", + return_value="Zg==", + ) + response = test_client.post( + "/v1/videos", + data={ + "prompt": "A mountain peak.", + "flow_shift": "0.5", + "extra_params": json.dumps({"use_zero_init": True, "zero_steps": 2}), + }, + ) + + assert response.status_code == 200 + video_id = response.json()["id"] + _wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value) + engine = test_client.app.state.openai_serving_video._engine_client + captured = engine.captured_sampling_params_list[0] + assert captured.extra_args["flow_shift"] == 0.5 + assert captured.extra_args["use_zero_init"] is True + assert captured.extra_args["zero_steps"] == 2 diff --git a/vllm_omni/entrypoints/openai/api_server.py b/vllm_omni/entrypoints/openai/api_server.py index ce387d404..82f05c53c 100644 --- a/vllm_omni/entrypoints/openai/api_server.py +++ b/vllm_omni/entrypoints/openai/api_server.py @@ -1714,16 +1714,22 @@ def _resolve_video_runtime_context(raw_request: Request) -> tuple[str | None, li return app_model_name, app_stage_configs -def _parse_form_json(value: str | None) -> Any: +def _parse_form_json(value: str | None, expected_type: type | None = None) -> Any: if value is None or value == "": return None try: - return json.loads(value) + parsed = json.loads(value) except json.JSONDecodeError as exc: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST.value, detail="Invalid JSON in form field.", ) from exc + if expected_type is not None and not isinstance(parsed, expected_type): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST.value, + detail=f"Invalid JSON in form field: expected {expected_type.__name__}, got {type(parsed).__name__}.", + ) + return parsed def video_response_from_request(model_name: str, req: VideoGenerationRequest) -> VideoResponse: @@ -1844,6 +1850,7 @@ async def create_video( seed: int | None = Form(default=None), negative_prompt: str | None = Form(default=None), lora: str | None = Form(default=None), + extra_params: str | None = Form(default=None), ) -> VideoResponse: """Create an asynchronous video generation job. @@ -1874,6 +1881,7 @@ async def create_video( seed: Optional random seed override. negative_prompt: Optional negative prompt. lora: Optional JSON-encoded per-request LoRA configuration. + extra_params: Optional model-specific parameters passed directly to the model's extra_args. Returns: A queued ``VideoResponse`` that includes the generated job identifier @@ -1910,7 +1918,8 @@ async def create_video( "true_cfg_scale": true_cfg_scale, "seed": seed, "negative_prompt": negative_prompt, - "lora": _parse_form_json(lora), + "lora": _parse_form_json(lora, expected_type=dict), + "extra_params": _parse_form_json(extra_params, expected_type=dict), } request_data = {k: v for k, v in request_data.items() if v is not None} diff --git a/vllm_omni/entrypoints/openai/protocol/videos.py b/vllm_omni/entrypoints/openai/protocol/videos.py index 1da6bdb08..e180bef22 100644 --- a/vllm_omni/entrypoints/openai/protocol/videos.py +++ b/vllm_omni/entrypoints/openai/protocol/videos.py @@ -161,6 +161,12 @@ class VideoGenerationRequest(BaseModel): ), ) + # Generic model-specific parameters + extra_params: dict[str, Any] | None = Field( + default=None, + description=("Optional model-specific parameters passed directly to the model's extra_args. "), + ) + def resolve_video_params(self) -> VideoParams: vp = VideoParams(width=self.width, height=self.height, fps=self.fps, num_frames=self.num_frames) diff --git a/vllm_omni/entrypoints/openai/serving_video.py b/vllm_omni/entrypoints/openai/serving_video.py index 48ffd4956..5ccc7c3d2 100644 --- a/vllm_omni/entrypoints/openai/serving_video.py +++ b/vllm_omni/entrypoints/openai/serving_video.py @@ -122,6 +122,17 @@ class OmniOpenAIServingVideo: if request.flow_shift is not None: gen_params.extra_args["flow_shift"] = request.flow_shift + # Apply model-specific extra parameters + if request.extra_params is not None: + if not isinstance(request.extra_params, dict): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST.value, + detail="extra_params must be a JSON object/dict.", + ) + # Merge extra_params into extra_args + gen_params.extra_args.update(request.extra_params) + logger.info("Applied extra_params: %s", request.extra_params) + self._apply_lora(request.lora, gen_params) logger.info(