Skip to content

Commit a159b63

Browse files
[bugfix] Harden OpenAI serving after post-merge review (#1782)
1 parent ac48bb3 commit a159b63

18 files changed

Lines changed: 540 additions & 108 deletions

docs/design/inference_schema_parity_inventory.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,6 @@ surfaces:
626626
max_sequence_length: request.sampling.max_sequence_length
627627
boundary_ratio: request.sampling.boundary_ratio
628628
extra_params: request.extensions
629-
output_path: request.output.output_path
630629
compatibility_only:
631630
seconds:
632631
target: request.sampling.num_frames

docs/design/server_contracts/openai.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ spellings.
7373
Reference objects support URL or local-path strings through `image_url`,
7474
`video_url`, and `audio_url`. `file_id` references are schema-compatible but
7575
return HTTP 400 because FastVideo does not provide an OpenAI Files store.
76-
Multipart `input_reference` uploads are saved under the configured output
77-
directory.
76+
Image URLs, data URLs, local paths, and multipart `input_reference` uploads are
77+
materialized and decoded under the configured output directory during
78+
admission. Invalid media returns HTTP 400 before a job is created.
7879

7980
## Jobs and synchronous responses
8081

@@ -85,7 +86,12 @@ timings, and peak-memory metadata when the pipeline reports them.
8586

8687
`POST /v1/videos/sync` returns `video/mp4` bytes. It includes
8788
`X-Request-Id`, `X-Model`, `X-Inference-Time-S`, `X-Stage-Durations`, and
88-
`X-Peak-Memory-MB` headers.
89+
`X-Peak-Memory-MB` headers. Its temporary MP4 is removed after the response is
90+
streamed. Asynchronous artifacts remain available until their job is deleted.
91+
92+
Output paths are controlled by the server. Clients cannot choose filesystem
93+
destinations; every video is written beneath `server.output_dir` with a unique
94+
request id.
8995

9096
FastVideo's synchronous CUDA execution cannot be interrupted after launch.
9197
Deleting an in-progress resource removes it from the API immediately; the
@@ -98,8 +104,8 @@ checkpoint path is used. Requests that name another model fail with HTTP 400.
98104

99105
LoRAs are configured under
100106
`generator.pipeline.components.{lora_path,lora_nickname,lora_strength}`. The
101-
startup adapter appears in `/v1/models`, and requests can use either its model
102-
nickname or a vLLM-Omni selector:
107+
startup adapter is the only model advertised by a LoRA server, and requests can
108+
select it by its model nickname or with a selector:
103109

104110
```json
105111
{
@@ -156,3 +162,9 @@ Errors use the OpenAI envelope:
156162
Parse, model-selection, startup-LoRA, and unsupported-parameter failures are
157163
HTTP 400; missing resources are HTTP 404; generation failures are stored on
158164
asynchronous jobs and returned as HTTP 500 when that job is retrieved.
165+
Unknown top-level fields are rejected. `extra_params` accepts only the explicit
166+
request-batch passthrough fields supported by the typed request adapter.
167+
168+
`GET /health` also verifies that the generation engine is open and all local
169+
multiprocess workers are alive. It returns HTTP 503 when the worker pool is no
170+
longer usable.

examples/serving/openai_fasth3_lora.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ server:
3434
host: 0.0.0.0
3535
port: 8000
3636
output_dir: outputs/openai_fasth3_lora
37-
served_model_name: minimax-h3
37+
served_model_name: fasth3-dense-datafree
3838

3939
default_request:
4040
negative_prompt: ""
@@ -49,4 +49,3 @@ default_request:
4949
seed: 1000
5050
output:
5151
return_frames: false
52-

fastvideo/entrypoints/cli/bench_serving.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,9 @@ async def benchmark(args: argparse.Namespace) -> None:
626626
) as resp:
627627
if resp.status == 200:
628628
info = await resp.json()
629-
if "model_path" in info and info["model_path"]:
629+
if info.get("served_model_name"):
630+
args.model = info["served_model_name"]
631+
elif info.get("model_path"):
630632
args.model = info["model_path"]
631633
logger.info("Updated model name from server: %s", args.model)
632634
except Exception as e:

fastvideo/entrypoints/openai/api_server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from contextlib import asynccontextmanager
55
from collections.abc import AsyncIterator
6+
import os
67

78
import uvicorn
89
from fastapi import FastAPI, HTTPException, Request
@@ -151,6 +152,11 @@ async def openai_validation_error(_request: Request, exc: RequestValidationError
151152

152153
@app.get("/health")
153154
async def health():
155+
from fastvideo.entrypoints.openai.state import get_serving_engine
156+
157+
engine = get_serving_engine()
158+
if not engine.healthy:
159+
raise HTTPException(status_code=503, detail=engine.unhealthy_reason or "generation engine is unhealthy")
154160
return {"status": "ok"}
155161

156162
return app
@@ -187,6 +193,7 @@ def run_server(
187193
served_model_name: str | None = None,
188194
):
189195
"""Create the app and run it with uvicorn"""
196+
os.environ.setdefault("FASTVIDEO_STAGE_LOGGING", "1")
190197
if default_request is not None:
191198
_validate_default_request_against_preset(default_request, fastvideo_args.model_path)
192199

fastvideo/entrypoints/openai/common_api.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ async def available_models():
2929
"""Show available models"""
3030
args = get_server_args()
3131
cards = [ModelCard(id=get_served_model_name(), root=args.model_path)]
32-
if args.lora_path and args.lora_nickname != get_served_model_name():
33-
cards.append(ModelCard(id=args.lora_nickname, root=args.model_path))
3432
return {"object": "list", "data": [card.model_dump() for card in cards]}
3533

3634

@@ -40,8 +38,6 @@ async def retrieve_model(model: str):
4038
args = get_server_args()
4139
served_model_name = get_served_model_name()
4240
available = {served_model_name}
43-
if args.lora_path:
44-
available.add(args.lora_nickname)
4541
if model not in available:
4642
return ORJSONResponse(
4743
status_code=404,

fastvideo/entrypoints/openai/protocol.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from enum import Enum
77
from typing import Annotated, Any, Literal
88

9-
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
9+
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, field_validator
1010

1111

1212
class ImageResponseData(BaseModel):
@@ -82,7 +82,7 @@ class FileImageReference(BaseModel):
8282

8383
class UrlImageReference(BaseModel):
8484
model_config = ConfigDict(extra="forbid")
85-
image_url: str
85+
image_url: str = Field(min_length=1)
8686

8787

8888
ImageReference = UrlImageReference | FileImageReference
@@ -95,15 +95,15 @@ class FileVideoReference(BaseModel):
9595

9696
class UrlVideoReference(BaseModel):
9797
model_config = ConfigDict(extra="forbid")
98-
video_url: str
98+
video_url: str = Field(min_length=1)
9999

100100

101101
VideoReference = UrlVideoReference | FileVideoReference
102102

103103

104104
class UrlAudioReference(BaseModel):
105105
model_config = ConfigDict(extra="forbid")
106-
audio_url: str
106+
audio_url: str = Field(min_length=1)
107107

108108

109109
AudioReference = UrlAudioReference
@@ -117,11 +117,11 @@ class VideoGenerationRequest(BaseModel):
117117
predate vLLM-Omni's typed reference objects.
118118
"""
119119

120-
model_config = ConfigDict(extra="allow")
120+
model_config = ConfigDict(extra="forbid")
121121

122-
prompt: str
122+
prompt: str = Field(min_length=1)
123123
model: str | None = None
124-
seconds: int | SecondStr | None = None
124+
seconds: Annotated[int, Field(ge=1, le=_INT64_MAX)] | SecondStr | None = None
125125
size: SizeStr | None = None
126126
image_reference: ImageReference | list[ImageReference] | None = None
127127
video_reference: VideoReference | list[VideoReference] | None = None
@@ -145,7 +145,7 @@ class VideoGenerationRequest(BaseModel):
145145
# SGLang spelling retained as an alias-like input field.
146146
n: int | None = Field(default=None, ge=1, le=10)
147147
start_time_seconds: float | None = Field(default=None, ge=0.0)
148-
quality: str | None = None
148+
quality: Literal["auto", "default", "standard", "hd"] | None = None
149149
negative_prompt: str | None = None
150150
num_inference_steps: int | None = Field(default=None, ge=1, le=200)
151151
guidance_scale: float | None = Field(default=None, ge=0.0, le=20.0)
@@ -166,7 +166,13 @@ class VideoGenerationRequest(BaseModel):
166166

167167
lora: dict[str, Any] | None = None
168168
extra_params: dict[str, Any] | None = None
169-
output_path: str | None = None
169+
170+
@field_validator("prompt")
171+
@classmethod
172+
def validate_prompt(cls, value: str) -> str:
173+
if not value.strip():
174+
raise ValueError("prompt must not be empty")
175+
return value
170176

171177
def resolve_video_params(self) -> VideoParams:
172178
"""Resolve top-level, nested, and ``size`` dimensions like vLLM-Omni."""

0 commit comments

Comments
 (0)