Files
hao-ai-lab-FastVideo/fastvideo/entrypoints/openai/api_server.py
T

222 lines
7.4 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
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
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("Loading model from %s ...", args.model_path)
generator = VideoGenerator.from_fastvideo_args(args)
serving_engine = OpenAIServingEngine(generator)
logger.info("Model loaded successfully.")
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,
) -> 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.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.video_api import router as video_router
app.include_router(common_router)
app.include_router(video_router)
app.include_router(image_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 _parse_args() -> tuple[FastVideoArgs, str, int, str]:
"""Parse CLI arguments and return (FastVideoArgs, host, port, output_dir)"""
from fastvideo.utils import FlexibleArgumentParser
parser = FlexibleArgumentParser(description="FastVideo OpenAI-compatible API server")
parser.add_argument("--host", type=str, default=DEFAULT_HOST)
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--output-dir", type=str, default=DEFAULT_OUTPUT_DIR)
parser = FastVideoArgs.add_cli_args(parser)
args = parser.parse_args()
host = args.host
port = args.port
output_dir = args.output_dir
# Build FastVideoArgs from the remaining CLI args
excluded = {"host", "port", "output_dir", "subparser", "config", "dispatch_function"}
cli_kwargs = {k: v for k, v in vars(args).items() if k not in excluded and v is not None}
fastvideo_args = FastVideoArgs.from_kwargs(**cli_kwargs)
return fastvideo_args, host, port, output_dir
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,
)
if __name__ == "__main__":
fastvideo_args, host, port, output_dir = _parse_args()
run_server(fastvideo_args, host, port, output_dir=output_dir)