Files
hao-ai-lab-FastVideo/fastvideo/entrypoints/openai/api_server.py
T
Davids048andClaude Opus 5.5 74a52c027b [refactor]: remove the flat-flag entry point of the OpenAI API server
python -m fastvideo.entrypoints.openai.api_server parsed the hand-written
FastVideoArgs argparse flags, an inference input path next to the typed config
that fastvideo serve reads. It is removed; start the server with
fastvideo serve --config SERVE_CONFIG and dotted overrides such as
--server.port 8000. The GPU integration test starts the server that way, with
the same model, GPU count, and DiT offload.

FastVideoArgs.add_cli_args stays for the legacy training and preprocessing
scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012M91wnVFmPEvJ9h39r5BH7
2026-10-03 01:54:34 +00:00

207 lines
7.0 KiB
Python

# Adapted from SGLang
# (https://github.com/sgl-project/sglang/blob/main/python/sglang/multimodal_gen/runtime/entrypoints/http_server.py)
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator, Callable
import os
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastvideo.api.presets import validate_preset_selection
from fastvideo.api.schema import GenerationRequest
from fastvideo.entrypoints.openai.state import (
DEFAULT_OUTPUT_DIR,
clear_state,
set_state,
)
from fastvideo.entrypoints.openai.serving_engine import OpenAIServingEngine, ServingGenerator
from fastvideo.entrypoints.openai.protocol import VideoGenerationRequest
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.logger import init_logger
from fastvideo.registry import get_preset_selection
logger = init_logger(__name__)
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8000
def _validate_default_request_against_preset(
default_request: GenerationRequest,
model_path: str,
) -> None:
"""Validate ``default_request.stage_overrides`` against the model's preset.
Called once at server startup from :func:`run_server`. The
``default_request`` is static server config, so validation results are
invariant across requests — there's no reason to re-run per request.
"""
if not default_request.stage_overrides:
return
preset_name, model_family = get_preset_selection(model_path)
if preset_name is None or model_family is None:
return
validate_preset_selection(
preset_name,
model_family,
stage_overrides=default_request.stage_overrides,
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Load model on startup, clean up on shutdown"""
args: FastVideoArgs = app.state.fastvideo_args
output_dir: str = app.state.output_dir
served_model_name: str | None = app.state.served_model_name
default_request: GenerationRequest | None = getattr(app.state, "default_request", None)
logger.info("Initializing %s generation runtime for %s ...", app.state.runtime, args.model_path)
factory = app.state.generator_factory
generator = factory() if factory is not None else VideoGenerator.from_fastvideo_args(args)
serving_engine = OpenAIServingEngine(generator, app.state.video_request_validator)
logger.info("Generation runtime ready.")
set_state(
generator,
serving_engine,
args,
output_dir,
default_request=default_request,
served_model_name=served_model_name,
)
try:
yield # server is running
finally:
logger.info("Shutting down — releasing model resources ...")
from fastvideo.entrypoints.openai.video_api import shutdown_video_jobs
await shutdown_video_jobs()
await serving_engine.shutdown()
clear_state()
logger.info("Shutdown complete.")
def create_app(
fastvideo_args: FastVideoArgs,
output_dir: str = DEFAULT_OUTPUT_DIR,
default_request: GenerationRequest | None = None,
served_model_name: str | None = None,
*,
generator_factory: Callable[[], ServingGenerator] | None = None,
video_request_validator: Callable[[VideoGenerationRequest], None] | None = None,
runtime: str = "cuda",
) -> FastAPI:
"""Build the FastAPI application with all routers mounted"""
app = FastAPI(
title="FastVideo OpenAI-Compatible API",
version="0.1.0",
lifespan=lifespan,
)
app.state.fastvideo_args = fastvideo_args
app.state.output_dir = output_dir
app.state.default_request = default_request
app.state.served_model_name = served_model_name
app.state.generator_factory = generator_factory
app.state.video_request_validator = video_request_validator
app.state.runtime = runtime
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(HTTPException)
async def openai_http_error(_request: Request, exc: HTTPException) -> JSONResponse:
"""Return the error envelope consumed by OpenAI-compatible clients."""
message = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
return JSONResponse(
status_code=exc.status_code,
headers=exc.headers,
content={
"error": {
"message": message,
"type": "invalid_request_error" if exc.status_code < 500 else "server_error",
"param": None,
"code": exc.status_code,
}
},
)
@app.exception_handler(RequestValidationError)
async def openai_validation_error(_request: Request, exc: RequestValidationError) -> JSONResponse:
return JSONResponse(
status_code=400,
content={"error": {
"message": str(exc),
"type": "invalid_request_error",
"param": None,
"code": 400,
}},
)
# Import and mount routers
from fastvideo.entrypoints.openai.common_api import router as common_router
from fastvideo.entrypoints.openai.image_api import router as image_router
from fastvideo.entrypoints.openai.playground import router as playground_router
from fastvideo.entrypoints.openai.video_api import router as video_router
app.include_router(common_router)
app.include_router(video_router)
if runtime != "mlx":
app.include_router(image_router)
app.include_router(playground_router)
@app.get("/health")
async def health():
from fastvideo.entrypoints.openai.state import get_serving_engine
engine = get_serving_engine()
if not engine.healthy:
raise HTTPException(status_code=503, detail=engine.unhealthy_reason or "generation engine is unhealthy")
return {"status": "ok"}
return app
def run_server(
fastvideo_args: FastVideoArgs,
host: str = DEFAULT_HOST,
port: int = DEFAULT_PORT,
output_dir: str = DEFAULT_OUTPUT_DIR,
default_request: GenerationRequest | None = None,
served_model_name: str | None = None,
):
"""Create the app and run it with uvicorn"""
os.environ.setdefault("FASTVIDEO_STAGE_LOGGING", "1")
if default_request is not None:
_validate_default_request_against_preset(default_request, fastvideo_args.model_path)
app = create_app(
fastvideo_args,
output_dir=output_dir,
default_request=default_request,
served_model_name=served_model_name,
)
logger.info("Starting FastVideo server on %s:%d", host, port)
logger.info("Model: %s", fastvideo_args.model_path)
uvicorn.run(
app,
host=host,
port=port,
log_level="info",
timeout_keep_alive=300,
)