2025-11-17 14:44:10 +08:00
import multiprocessing
import multiprocessing.forkserver as forkserver
import os
2025-12-22 20:05:37 -05:00
# Image generation API imports
import time
2025-11-17 14:44:10 +08:00
from argparse import Namespace
2025-11-18 20:54:26 +08:00
from collections.abc import AsyncIterator
2025-11-17 14:44:10 +08:00
from contextlib import asynccontextmanager
from http import HTTPStatus
2025-12-16 15:19:40 +08:00
from typing import Any
2025-11-17 14:44:10 +08:00
2025-11-18 20:54:26 +08:00
import vllm.envs as envs
2025-11-17 14:44:10 +08:00
from fastapi import Depends , HTTPException , Request
from fastapi.responses import JSONResponse , StreamingResponse
from starlette.datastructures import State
from vllm.config import VllmConfig
from vllm.engine.protocol import EngineClient
2025-11-18 20:54:26 +08:00
from vllm.entrypoints.chat_utils import load_chat_template , resolve_hf_chat_template , resolve_mistral_chat_template
2025-11-17 14:44:10 +08:00
from vllm.entrypoints.launcher import serve_http
from vllm.entrypoints.logger import RequestLogger
from vllm.entrypoints.openai.api_server import (
base ,
build_app ,
load_log_config ,
maybe_register_tokenizer_info_endpoint ,
router ,
setup_server ,
validate_json_request ,
)
2025-11-18 20:54:26 +08:00
from vllm.entrypoints.openai.protocol import ChatCompletionRequest , ChatCompletionResponse , ErrorResponse
from vllm.entrypoints.openai.serving_models import BaseModelPath , LoRAModulePath , OpenAIServingModels
2025-11-17 14:44:10 +08:00
from vllm.entrypoints.openai.tool_parsers import ToolParserManager
# yapf conflicts with isort for this block
# yapf: disable
# yapf: enable
from vllm.entrypoints.tool_server import DemoToolServer , MCPToolServer , ToolServer
from vllm.entrypoints.utils import load_aware_call , with_cancellation
from vllm.logger import init_logger
2025-12-19 16:42:02 +08:00
from vllm.tokenizers import MistralTokenizer
from vllm.utils.system_utils import decorate_logs
2025-11-18 20:54:26 +08:00
2025-11-30 22:26:33 +08:00
from vllm_omni.entrypoints.async_omni import AsyncOmni
2025-12-22 20:05:37 -05:00
from vllm_omni.entrypoints.openai.image_api_utils import (
encode_image_base64 ,
parse_size ,
)
2025-12-27 16:56:51 +11:00
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
2025-12-22 20:05:37 -05:00
from vllm_omni.entrypoints.openai.protocol.images import (
ImageData ,
ImageGenerationRequest ,
ImageGenerationResponse ,
)
2025-11-17 14:44:10 +08:00
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
2025-12-27 16:56:51 +11:00
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
2025-11-17 14:44:10 +08:00
logger = init_logger ( __name__ )
2025-12-22 20:05:37 -05:00
# Server entry points
2025-11-17 14:44:10 +08:00
async def omni_run_server ( args , ** uvicorn_kwargs ) -> None :
2025-12-17 17:40:45 +08:00
"""Run a single-worker API server.
2025-12-26 20:59:33 +08:00
Unified entry point that automatically handles both LLM and Diffusion models
through AsyncOmni, which manages multi-stage pipelines.
2025-12-17 17:40:45 +08:00
"""
2025-12-26 20:59:33 +08:00
# Suppress Pydantic serialization warnings globally for multimodal content
# (e.g., when ChatMessage.content is a list instead of str)
import warnings as warnings_module
warnings_module . filterwarnings ( "ignore" , message = ".*Pydantic.*serialization.*" , category = UserWarning )
warnings_module . filterwarnings ( "ignore" , message = ".*PydanticSerializationUnexpectedValue.*" , category = UserWarning )
2025-11-17 14:44:10 +08:00
# Add process-specific prefix to stdout and stderr.
decorate_logs ( "APIServer" )
listen_address , sock = setup_server ( args )
2025-12-17 17:40:45 +08:00
2025-12-26 20:59:33 +08:00
# Unified use of omni_run_server_worker, AsyncOmni automatically handles LLM and Diffusion models
await omni_run_server_worker ( listen_address , sock , args , ** uvicorn_kwargs )
2025-11-17 14:44:10 +08:00
2025-11-18 20:54:26 +08:00
async def omni_run_server_worker ( listen_address , sock , args , client_config = None , ** uvicorn_kwargs ) -> None :
2025-11-17 14:44:10 +08:00
"""Run a single API server worker."""
if args . tool_parser_plugin and len ( args . tool_parser_plugin ) > 3 :
ToolParserManager . import_tool_parser ( args . tool_parser_plugin )
# Load logging config for uvicorn if specified
log_config = load_log_config ( args . log_config_file )
if log_config is not None :
uvicorn_kwargs [ "log_config" ] = log_config
2025-11-30 22:26:33 +08:00
async with build_async_omni (
2025-11-17 14:44:10 +08:00
args ,
client_config = client_config ,
) as engine_client :
maybe_register_tokenizer_info_endpoint ( args )
app = build_app ( args )
vllm_config = await engine_client . get_vllm_config ()
await omni_init_app_state ( engine_client , vllm_config , app . state , args )
2025-12-26 20:59:33 +08:00
# Check if pure diffusion mode (vllm_config will be None)
is_pure_diffusion = vllm_config is None
if is_pure_diffusion :
logger . info (
"Starting vLLM API server (pure diffusion mode) on %s " ,
listen_address ,
)
else :
logger . info (
"Starting vLLM API server %d on %s " ,
vllm_config . parallel_config . _api_process_rank ,
listen_address ,
)
2025-11-17 14:44:10 +08:00
shutdown_task = await serve_http (
app ,
sock = sock ,
enable_ssl_refresh = args . enable_ssl_refresh ,
host = args . host ,
port = args . port ,
log_level = args . uvicorn_log_level ,
# NOTE: When the 'disable_uvicorn_access_log' value is True,
# no access log will be output.
access_log = not args . disable_uvicorn_access_log ,
timeout_keep_alive = envs . VLLM_HTTP_TIMEOUT_KEEP_ALIVE ,
ssl_keyfile = args . ssl_keyfile ,
ssl_certfile = args . ssl_certfile ,
ssl_ca_certs = args . ssl_ca_certs ,
ssl_cert_reqs = args . ssl_cert_reqs ,
h11_max_incomplete_event_size = args . h11_max_incomplete_event_size ,
h11_max_header_count = args . h11_max_header_count ,
** uvicorn_kwargs ,
)
# NB: Await server shutdown only after the backend context is exited
try :
await shutdown_task
finally :
sock . close ()
@asynccontextmanager
2025-11-30 22:26:33 +08:00
async def build_async_omni (
2025-11-17 14:44:10 +08:00
args : Namespace ,
* ,
2025-12-16 15:19:40 +08:00
disable_frontend_multiprocessing : bool | None = None ,
client_config : dict [ str , Any ] | None = None ,
2025-11-17 14:44:10 +08:00
) -> AsyncIterator [ EngineClient ]:
2025-11-30 22:26:33 +08:00
"""Build an AsyncOmni instance from command-line arguments.
2025-11-28 11:03:52 +08:00
2025-11-30 22:26:33 +08:00
Creates an async context manager that yields an AsyncOmni instance
2025-11-28 11:03:52 +08:00
configured from the provided arguments. Handles forkserver setup if
needed and ensures proper cleanup on exit.
Args:
args: Parsed command-line arguments containing model and configuration
disable_frontend_multiprocessing: Optional flag to disable frontend
multiprocessing (deprecated in V1)
client_config: Optional client configuration dictionary
Yields:
2025-11-30 22:26:33 +08:00
EngineClient instance (AsyncOmni) ready for use
2025-11-28 11:03:52 +08:00
"""
2025-11-17 14:44:10 +08:00
if os . getenv ( "VLLM_WORKER_MULTIPROC_METHOD" ) == "forkserver" :
# The executor is expected to be mp.
# Pre-import heavy modules in the forkserver process
logger . debug ( "Setup forkserver with pre-imports" )
multiprocessing . set_start_method ( "forkserver" )
multiprocessing . set_forkserver_preload ([ "vllm.v1.engine.async_llm" ])
forkserver . ensure_running ()
logger . debug ( "Forkserver setup complete!" )
2025-11-30 22:26:33 +08:00
# Context manager to handle async_omni lifecycle
2025-11-17 14:44:10 +08:00
# Ensures everything is shutdown and cleaned up on error/exit
2025-11-30 22:26:33 +08:00
async with build_async_omni_from_stage_config (
2025-11-17 14:44:10 +08:00
args ,
disable_frontend_multiprocessing = disable_frontend_multiprocessing ,
2025-11-30 22:26:33 +08:00
) as async_omni :
yield async_omni
2025-11-17 14:44:10 +08:00
@asynccontextmanager
2025-11-30 22:26:33 +08:00
async def build_async_omni_from_stage_config (
2025-11-17 14:44:10 +08:00
args : Namespace ,
* ,
disable_frontend_multiprocessing : bool = False ,
) -> AsyncIterator [ EngineClient ]:
2025-11-30 22:26:33 +08:00
"""Create AsyncOmni from stage configuration.
2025-11-17 14:44:10 +08:00
2025-11-30 22:26:33 +08:00
Creates an AsyncOmni instance either in-process or using multiprocess
2025-11-28 11:03:52 +08:00
RPC. Loads stage configurations from the model or from a specified path.
Args:
args: Parsed command-line arguments containing model and stage configs
disable_frontend_multiprocessing: Flag to disable frontend multiprocessing
(deprecated in V1)
client_config: Optional client configuration dictionary
Yields:
2025-11-30 22:26:33 +08:00
EngineClient instance (AsyncOmni) ready for use
2025-11-28 11:03:52 +08:00
Note:
Stage configurations are loaded from args.stage_configs_path if provided,
otherwise from the model's default configuration.
2025-11-17 14:44:10 +08:00
"""
# V1 AsyncLLM.
if disable_frontend_multiprocessing :
2025-12-19 16:42:02 +08:00
logger . warning ( "V1 is enabled, but got --disable-frontend-multiprocessing." )
2025-11-17 14:44:10 +08:00
2025-12-16 15:19:40 +08:00
async_omni : EngineClient | None = None
2025-11-17 14:44:10 +08:00
try :
2025-12-26 20:59:33 +08:00
# Convert args Namespace to kwargs dict for AsyncOmni to use
kwargs = vars ( args ) . copy ()
# Remove model as it will be passed separately
kwargs . pop ( "model" , None )
async_omni = AsyncOmni ( model = args . model , ** kwargs )
2025-11-17 14:44:10 +08:00
# # Don't keep the dummy data in memory
# await async_llm.reset_mm_cache()
2025-11-30 22:26:33 +08:00
yield async_omni
2025-11-17 14:44:10 +08:00
finally :
2025-11-30 22:26:33 +08:00
if async_omni :
async_omni . shutdown ()
2025-11-17 14:44:10 +08:00
async def omni_init_app_state (
engine_client : EngineClient ,
2025-12-26 20:59:33 +08:00
vllm_config : VllmConfig | None ,
2025-11-17 14:44:10 +08:00
state : State ,
args : Namespace ,
) -> None :
2025-11-28 11:03:52 +08:00
"""Initialize the FastAPI application state for omni API server.
Sets up the application state with model information, request logger,
and other server configuration needed for handling API requests.
2025-12-26 20:59:33 +08:00
Automatically detects pure diffusion mode (single diffusion stage) and
handles it appropriately.
2025-11-28 11:03:52 +08:00
Args:
2025-11-30 22:26:33 +08:00
engine_client: Engine client instance (AsyncOmni)
2025-12-26 20:59:33 +08:00
vllm_config: vLLM configuration object (may be None for pure diffusion)
2025-11-28 11:03:52 +08:00
state: FastAPI application state object to initialize
args: Parsed command-line arguments
"""
2025-12-26 20:59:33 +08:00
# Detect if it's pure Diffusion mode (single stage and is Diffusion)
is_pure_diffusion = False
if hasattr ( engine_client , "stage_configs" ) and engine_client . stage_configs :
stage_configs = engine_client . stage_configs
if len ( stage_configs ) == 1 :
stage_type = stage_configs [ 0 ] . get ( "stage_type" , "llm" )
if stage_type == "diffusion" :
is_pure_diffusion = True
logger . info ( "Detected pure diffusion mode (single diffusion stage)" )
2025-11-17 14:44:10 +08:00
if args . served_model_name is not None :
served_model_names = args . served_model_name
else :
served_model_names = [ args . model ]
if args . enable_log_requests :
request_logger = RequestLogger ( max_log_len = args . max_log_len )
else :
request_logger = None
2025-11-18 20:54:26 +08:00
base_model_paths = [ BaseModelPath ( name = name , model_path = args . model ) for name in served_model_names ]
2025-11-17 14:44:10 +08:00
state . engine_client = engine_client
state . log_stats = not args . disable_log_stats
# For omni models
2025-12-26 20:59:33 +08:00
state . stage_configs = engine_client . stage_configs if hasattr ( engine_client , "stage_configs" ) else None
# Pure Diffusion mode: use simplified initialization logic
if is_pure_diffusion :
model_name = served_model_names [ 0 ] if served_model_names else args . model
state . vllm_config = None
state . diffusion_engine = engine_client
# Use for_diffusion method to create chat handler
state . openai_serving_chat = OmniOpenAIServingChat . for_diffusion (
diffusion_engine = engine_client , # type: ignore
model_name = model_name ,
)
state . enable_server_load_tracking = getattr ( args , "enable_server_load_tracking" , False )
state . server_load_metrics = 0
logger . info ( "Pure diffusion API server initialized for model: %s " , model_name )
return
# LLM or multi-stage mode: use standard initialization logic
if vllm_config is None :
# Try to get vllm_config from engine_client
vllm_config = await engine_client . get_vllm_config ()
if vllm_config is None :
logger . warning ( "vllm_config is None, some features may not work correctly" )
state . vllm_config = vllm_config
if vllm_config is not None :
_model_config = vllm_config . model_config
2025-11-17 14:44:10 +08:00
resolved_chat_template = load_chat_template ( args . chat_template )
2025-12-26 20:59:33 +08:00
if resolved_chat_template is not None and vllm_config is not None :
2025-11-17 14:44:10 +08:00
# Get the tokenizer to check official template
tokenizer = await engine_client . get_tokenizer ()
2025-12-26 20:59:33 +08:00
if tokenizer is not None :
if isinstance ( tokenizer , MistralTokenizer ):
# The warning is logged in resolve_mistral_chat_template.
resolved_chat_template = resolve_mistral_chat_template ( chat_template = resolved_chat_template )
else :
hf_chat_template = resolve_hf_chat_template (
tokenizer = tokenizer ,
chat_template = None ,
tools = None ,
model_config = vllm_config . model_config ,
2025-11-17 14:44:10 +08:00
)
2025-12-26 20:59:33 +08:00
if hf_chat_template != resolved_chat_template :
logger . warning (
"Using supplied chat template: %s \n It is different from official chat template ' %s '. This discrepancy may lead to performance degradation." , # noqa: E501
resolved_chat_template ,
args . model ,
)
2025-11-17 14:44:10 +08:00
if args . tool_server == "demo" :
2025-12-16 15:19:40 +08:00
tool_server : ToolServer | None = DemoToolServer ()
2025-11-17 14:44:10 +08:00
assert isinstance ( tool_server , DemoToolServer )
await tool_server . init_and_validate ()
elif args . tool_server :
tool_server = MCPToolServer ()
await tool_server . add_tool_server ( args . tool_server )
else :
tool_server = None
# Merge default_mm_loras into the static lora_modules
2025-12-26 20:59:33 +08:00
default_mm_loras = {}
if vllm_config is not None and vllm_config . lora_config is not None :
default_mm_loras = vllm_config . lora_config . default_mm_loras
2025-11-17 14:44:10 +08:00
lora_modules = args . lora_modules
if default_mm_loras :
default_mm_lora_paths = [
LoRAModulePath (
name = modality ,
path = lora_path ,
)
for modality , lora_path in default_mm_loras . items ()
]
if args . lora_modules is None :
lora_modules = default_mm_lora_paths
else :
lora_modules += default_mm_lora_paths
2025-12-26 20:59:33 +08:00
# Ensure input_processor, io_processor, and model_config exist for OpenAIServingModels compatibility
if (
not hasattr ( engine_client , "input_processor" )
or engine_client . input_processor is None
or not hasattr ( engine_client , "io_processor" )
or engine_client . io_processor is None
or not hasattr ( engine_client , "model_config" )
or engine_client . model_config is None
):
if vllm_config is not None :
# Try to initialize processors if vllm_config is available
try :
from vllm.plugins.io_processors import get_io_processor
from vllm_omni.engine.input_processor import OmniInputProcessor
tokenizer = await engine_client . get_tokenizer ()
if tokenizer is not None :
# Initialize input_processor
if not hasattr ( engine_client , "input_processor" ) or engine_client . input_processor is None :
engine_client . input_processor = OmniInputProcessor (
vllm_config = vllm_config ,
tokenizer = tokenizer ,
)
logger . info ( "Initialized input_processor for AsyncOmni" )
# Initialize model_config
if not hasattr ( engine_client , "model_config" ) or engine_client . model_config is None :
engine_client . model_config = vllm_config . model_config
logger . info ( "Initialized model_config for AsyncOmni" )
# Initialize io_processor
if not hasattr ( engine_client , "io_processor" ) or engine_client . io_processor is None :
model_config = (
engine_client . model_config
if hasattr ( engine_client , "model_config" )
else vllm_config . model_config
)
io_processor_plugin = model_config . io_processor_plugin
engine_client . io_processor = get_io_processor ( vllm_config , io_processor_plugin )
logger . info ( "Initialized io_processor for AsyncOmni" )
else :
logger . warning ( "Cannot initialize processors: tokenizer is None. OpenAIServingModels may fail." )
except Exception as e :
logger . warning (
"Failed to initialize processors for AsyncOmni: %s . OpenAIServingModels may fail." ,
e ,
)
else :
logger . warning ( "Cannot initialize processors: vllm_config is None. OpenAIServingModels may fail." )
2025-11-17 14:44:10 +08:00
state . openai_serving_models = OpenAIServingModels (
engine_client = engine_client ,
base_model_paths = base_model_paths ,
lora_modules = lora_modules ,
)
await state . openai_serving_models . init_static_loras ()
state . openai_serving_chat = OmniOpenAIServingChat (
engine_client ,
state . openai_serving_models ,
args . response_role ,
request_logger = request_logger ,
chat_template = resolved_chat_template ,
chat_template_content_format = args . chat_template_content_format ,
trust_request_chat_template = args . trust_request_chat_template ,
return_tokens_as_token_ids = args . return_tokens_as_token_ids ,
enable_auto_tools = args . enable_auto_tool_choice ,
exclude_tools_when_tool_choice_none = args . exclude_tools_when_tool_choice_none ,
tool_parser = args . tool_call_parser ,
reasoning_parser = args . structured_outputs_config . reasoning_parser ,
enable_prompt_tokens_details = args . enable_prompt_tokens_details ,
enable_force_include_usage = args . enable_force_include_usage ,
enable_log_outputs = args . enable_log_outputs ,
log_error_stack = args . log_error_stack ,
)
2025-12-27 16:56:51 +11:00
state . openai_serving_speech = OmniOpenAIServingSpeech (
engine_client , state . openai_serving_models , request_logger = request_logger
)
2025-11-17 14:44:10 +08:00
state . enable_server_load_tracking = args . enable_server_load_tracking
state . server_load_metrics = 0
2025-12-16 15:19:40 +08:00
def Omnichat ( request : Request ) -> OmniOpenAIServingChat | None :
2025-11-17 14:44:10 +08:00
return request . app . state . openai_serving_chat
2025-12-27 16:56:51 +11:00
def Omnispeech ( request : Request ) -> OmniOpenAIServingSpeech | None :
return request . app . state . openai_serving_speech
2025-11-17 14:44:10 +08:00
@router.post (
"/v1/chat/completions" ,
dependencies = [ Depends ( validate_json_request )],
responses = {
HTTPStatus . OK . value : { "content" : { "text/event-stream" : {}}},
HTTPStatus . BAD_REQUEST . value : { "model" : ErrorResponse },
HTTPStatus . NOT_FOUND . value : { "model" : ErrorResponse },
HTTPStatus . INTERNAL_SERVER_ERROR . value : { "model" : ErrorResponse },
},
)
@with_cancellation
@load_aware_call
async def create_chat_completion ( request : ChatCompletionRequest , raw_request : Request ):
handler = Omnichat ( raw_request )
if handler is None :
2025-11-18 20:54:26 +08:00
return base ( raw_request ) . create_error_response ( message = "The model does not support Chat Completions API" )
2025-11-17 14:44:10 +08:00
try :
generator = await handler . create_chat_completion ( request , raw_request )
except Exception as e :
2025-12-17 17:40:45 +08:00
logger . exception ( "Chat completion failed: %s " , e )
2025-11-18 20:54:26 +08:00
raise HTTPException ( status_code = HTTPStatus . INTERNAL_SERVER_ERROR . value , detail = str ( e )) from e
2025-12-17 17:40:45 +08:00
2025-11-17 14:44:10 +08:00
if isinstance ( generator , ErrorResponse ):
2025-12-17 17:40:45 +08:00
return JSONResponse (
content = generator . model_dump (), status_code = generator . code if hasattr ( generator , "code" ) else 400
)
2025-11-17 14:44:10 +08:00
elif isinstance ( generator , ChatCompletionResponse ):
2025-12-26 20:59:33 +08:00
# Completely bypass Pydantic serialization warnings for multimodal content
# by converting to dict first, then serializing with warnings suppressed
import json as json_lib
import warnings as warnings_module
# Temporarily suppress ALL Pydantic UserWarnings during serialization
with warnings_module . catch_warnings ():
warnings_module . filterwarnings ( "ignore" , category = UserWarning )
warnings_module . filterwarnings ( "ignore" , message = ".*Pydantic.*" , category = UserWarning )
try :
# Use serialize_as_any=True to bypass type checking
response_dict = generator . model_dump ( mode = "json" , serialize_as_any = True , warnings = "none" )
return JSONResponse ( content = response_dict )
except Exception :
# Fallback: convert to JSON string and parse back to avoid any serialization issues
try :
response_json = generator . model_dump_json ( warnings = "none" , serialize_as_any = True )
response_dict = json_lib . loads ( response_json )
return JSONResponse ( content = response_dict )
except Exception :
# Last resort: regular dump with warnings suppressed
with warnings_module . catch_warnings ():
warnings_module . filterwarnings ( "ignore" , category = UserWarning )
return JSONResponse ( content = generator . model_dump ( mode = "json" , warnings = "none" ))
2025-11-17 14:44:10 +08:00
return StreamingResponse ( content = generator , media_type = "text/event-stream" )
2025-12-22 20:05:37 -05:00
2025-12-27 16:56:51 +11:00
@router.post (
"/v1/audio/speech" ,
dependencies = [ Depends ( validate_json_request )],
responses = {
HTTPStatus . OK . value : { "content" : { "audio/*" : {}}},
HTTPStatus . BAD_REQUEST . value : { "model" : ErrorResponse },
HTTPStatus . NOT_FOUND . value : { "model" : ErrorResponse },
HTTPStatus . INTERNAL_SERVER_ERROR . value : { "model" : ErrorResponse },
},
)
@with_cancellation
@load_aware_call
async def create_speech ( request : OpenAICreateSpeechRequest , raw_request : Request ):
handler = Omnispeech ( raw_request )
if handler is None :
return base ( raw_request ) . create_error_response ( message = "The model does not support Speech API" )
try :
return await handler . create_speech ( request , raw_request )
except Exception as e :
raise HTTPException ( status_code = HTTPStatus . INTERNAL_SERVER_ERROR . value , detail = str ( e )) from e
2025-12-22 20:05:37 -05:00
# Image generation API endpoints
@router.post (
"/v1/images/generations" ,
dependencies = [ Depends ( validate_json_request )],
responses = {
HTTPStatus . OK . value : { "model" : ImageGenerationResponse },
HTTPStatus . BAD_REQUEST . value : { "model" : ErrorResponse },
HTTPStatus . SERVICE_UNAVAILABLE . value : { "model" : ErrorResponse },
HTTPStatus . INTERNAL_SERVER_ERROR . value : { "model" : ErrorResponse },
},
)
async def generate_images ( request : ImageGenerationRequest , raw_request : Request ) -> ImageGenerationResponse :
"""Generate images from text prompts using diffusion models.
OpenAI DALL-E compatible endpoint for text-to-image generation.
2025-12-26 20:59:33 +08:00
Only supports multi-stage omni mode with diffusion stages.
2025-12-22 20:05:37 -05:00
Args:
request: Image generation request with prompt and parameters
raw_request: Raw FastAPI request for accessing app state
Returns:
ImageGenerationResponse with generated images as base64 PNG
Raises:
HTTPException: For validation errors, missing engine, or generation failures
"""
2025-12-26 20:59:33 +08:00
# Get engine client (AsyncOmni) from app state
engine_client : EngineClient | None = getattr ( raw_request . app . state , "engine_client" , None )
if engine_client is None or not hasattr ( engine_client , "stage_list" ):
2025-12-22 20:05:37 -05:00
raise HTTPException (
status_code = HTTPStatus . SERVICE_UNAVAILABLE . value ,
2025-12-26 20:59:33 +08:00
detail = "Multi-stage engine not initialized. Start server with a multi-stage omni model." ,
2025-12-22 20:05:37 -05:00
)
2025-12-26 20:59:33 +08:00
# Check if there's a diffusion stage
stage_configs = getattr ( raw_request . app . state , "stage_configs" , None )
if not stage_configs :
raise HTTPException (
status_code = HTTPStatus . SERVICE_UNAVAILABLE . value ,
detail = "Stage configs not found. Start server with a multi-stage omni model." ,
)
2025-12-30 16:50:30 +08:00
# Check for diffusion stage and collect stage types
2025-12-26 20:59:33 +08:00
has_diffusion_stage = False
2025-12-30 16:50:30 +08:00
stage_types : list [ str ] = []
2025-12-26 20:59:33 +08:00
for stage in stage_configs :
# Handle both dict and OmegaConf objects
stage_type = None
if isinstance ( stage , dict ):
stage_type = stage . get ( "stage_type" , "llm" )
elif hasattr ( stage , "get" ):
stage_type = stage . get ( "stage_type" , "llm" )
elif hasattr ( stage , "stage_type" ):
stage_type = stage . stage_type
else :
# Fallback: try to access as dict-like
try :
stage_type = stage [ "stage_type" ] if "stage_type" in stage else "llm"
except ( TypeError , KeyError ):
stage_type = "llm"
if stage_type == "diffusion" :
has_diffusion_stage = True
2025-12-30 16:50:30 +08:00
stage_types . append ( stage_type )
2025-12-26 20:59:33 +08:00
if not has_diffusion_stage :
raise HTTPException (
status_code = HTTPStatus . SERVICE_UNAVAILABLE . value ,
detail = "No diffusion stage found in multi-stage pipeline." ,
)
# Get server's loaded model name
serving_models = getattr ( raw_request . app . state , "openai_serving_models" , None )
if serving_models and hasattr ( serving_models , "base_model_paths" ) and serving_models . base_model_paths :
model_name = serving_models . base_model_paths [ 0 ] . name
else :
model_name = "unknown"
2025-12-22 20:05:37 -05:00
# Validate model field (warn if mismatch, don't error)
if request . model is not None and request . model != model_name :
logger . warning (
f "Model mismatch: request specifies ' { request . model } ' but "
f "server is running ' { model_name } '. Using server model."
)
try :
# Build params - pass through user values directly
gen_params = {
"prompt" : request . prompt ,
"num_outputs_per_prompt" : request . n ,
}
# Parse and add size if provided
if request . size :
width , height = parse_size ( request . size )
gen_params [ "height" ] = height
gen_params [ "width" ] = width
size_str = f " { width } x { height } "
else :
size_str = "model default"
# Add optional parameters ONLY if provided
if request . num_inference_steps is not None :
gen_params [ "num_inference_steps" ] = request . num_inference_steps
if request . negative_prompt is not None :
gen_params [ "negative_prompt" ] = request . negative_prompt
if request . guidance_scale is not None :
gen_params [ "guidance_scale" ] = request . guidance_scale
if request . true_cfg_scale is not None :
gen_params [ "true_cfg_scale" ] = request . true_cfg_scale
if request . seed is not None :
gen_params [ "seed" ] = request . seed
2025-12-26 20:59:33 +08:00
gen_params [ "request_id" ] = f "img_gen_ { int ( time . time ()) } "
2025-12-22 20:05:37 -05:00
logger . info ( f "Generating { request . n } image(s) { size_str } " )
2025-12-26 20:59:33 +08:00
# Generate images using AsyncOmni (multi-stage mode)
2025-12-30 16:50:30 +08:00
result = None
stage_list = getattr ( engine_client , "stage_list" , None )
if isinstance ( stage_list , list ):
default_params_list = getattr ( engine_client , "default_sampling_params_list" , None )
if not isinstance ( default_params_list , list ):
default_params_list = [{} for _ in stage_types ]
else :
default_params_list = list ( default_params_list )
if len ( default_params_list ) != len ( stage_types ):
default_params_list = ( default_params_list + [{} for _ in stage_types ])[: len ( stage_types )]
sampling_params_list : list [ dict [ str , Any ]] = []
for idx , stage_type in enumerate ( stage_types ):
if stage_type == "diffusion" :
sampling_params_list . append ( gen_params )
else :
base_params = default_params_list [ idx ]
sampling_params_list . append ( dict ( base_params ) if isinstance ( base_params , dict ) else base_params )
async for output in engine_client . generate (
prompt = gen_params [ "prompt" ],
request_id = gen_params [ "request_id" ],
sampling_params_list = sampling_params_list ,
):
result = output
else :
result = await engine_client . generate ( ** gen_params )
2025-12-26 20:59:33 +08:00
if result is None :
raise HTTPException (
status_code = HTTPStatus . INTERNAL_SERVER_ERROR . value ,
detail = "No output generated from multi-stage pipeline." ,
)
2025-12-22 20:05:37 -05:00
# Extract images from result
2025-12-30 16:50:30 +08:00
images = []
if hasattr ( result , "images" ) and result . images :
images = result . images
elif hasattr ( result , "request_output" ):
request_output = result . request_output
if isinstance ( request_output , dict ) and request_output . get ( "images" ):
images = request_output [ "images" ]
elif hasattr ( request_output , "images" ) and request_output . images :
images = request_output . images
2025-12-22 20:05:37 -05:00
logger . info ( f "Successfully generated { len ( images ) } image(s)" )
# Encode images to base64
image_data = [ ImageData ( b64_json = encode_image_base64 ( img ), revised_prompt = None ) for img in images ]
return ImageGenerationResponse (
created = int ( time . time ()),
data = image_data ,
)
except HTTPException :
# Re-raise HTTPExceptions as-is
raise
except ValueError as e :
logger . error ( f "Validation error: { e } " )
raise HTTPException ( status_code = HTTPStatus . BAD_REQUEST . value , detail = str ( e ))
except Exception as e :
logger . exception ( f "Image generation failed: { e } " )
raise HTTPException (
status_code = HTTPStatus . INTERNAL_SERVER_ERROR . value , detail = f "Image generation failed: { str ( e ) } "
)