"""Python entrypoint of router.""" from collections.abc import AsyncGenerator from http import HTTPStatus from typing import List, Literal, Optional, Type # noqa: UP035 import fastapi import uvicorn from fastapi.middleware.cors import CORSMiddleware from mlc_llm.protocol import error_protocol from mlc_llm.protocol.openai_api_protocol import CompletionLogProbs, CompletionRequest from mlc_llm.router import Router from mlc_llm.serve import engine_base, engine_utils def serve( model: str, model_lib: Optional[str], router_host: str, router_port: int, endpoint_hosts: List[str], # noqa: UP006 endpoint_ports: List[int], # noqa: UP006 endpoint_num_gpus: List[int], # noqa: UP006 enable_prefix_cache: bool, router_mode: Literal["disagg", "round-robin"] = "round-robin", pd_balance_factor: float = 0.0, router_type: Type[Router] = Router, # noqa: UP006 ): """Start the router with the specified configuration.""" # 1. Instantiate router router = router_type( model=model, model_lib=model_lib, hosts=endpoint_hosts, ports=endpoint_ports, num_gpus=endpoint_num_gpus, enable_prefix_cache=enable_prefix_cache, router_mode=router_mode, pd_balance_factor=pd_balance_factor, ) router_app = fastapi.APIRouter() @router_app.post("/v1/completions") async def request_completion(request: CompletionRequest, raw_request: fastapi.Request): """OpenAI-compatible completion API. API reference: https://platform.openai.com/docs/api-reference/completions/create """ if router is None: return error_protocol.create_error_response( HTTPStatus.BAD_REQUEST, message="Router is not initialized." ) request_id = f"cmpl-{engine_utils.random_uuid()}" # Streaming response. if request.stream: # We manually get the first response from generator to # capture potential exceptions in this scope, rather then # the StreamingResponse scope. stream_generator = router.handle_completion(request, request_id) first_response = await anext( # noqa: F821 stream_generator ) async def completion_stream_generator() -> AsyncGenerator[str, None]: if isinstance(first_response, StopAsyncIteration): yield "data: [DONE]\n\n" return yield f"data: {first_response.model_dump_json(by_alias=True)}\n\n" async for response in stream_generator: yield f"data: {response.model_dump_json(by_alias=True)}\n\n" yield "data: [DONE]\n\n" return fastapi.responses.StreamingResponse( completion_stream_generator(), media_type="text/event-stream" ) # FIXME: Non-streaming response not fully implemented request_final_usage = None output_texts = [""] * request.n finish_reasons: List[Optional[str]] = [None] * request.n # noqa: UP006 logprob_results: List[Optional[CompletionLogProbs]] = [None] * request.n # noqa: UP006 async for response in router.handle_completion(request, request_id): if await raw_request.is_disconnected(): # In non-streaming cases, the engine will not be notified # when the request is disconnected. # Therefore, we check if it is disconnected each time, # and explicitly return. # Note that requesta abort is triggered when the async for and funciton scope ends. return error_protocol.create_error_response( HTTPStatus.BAD_REQUEST, message="The request has disconnected" ) # TODO(Charlie): This is copied from engine.py -- # why is it here? Non-streaming only has a single chunk right? # this is the final chunk # if response.usage is not None: # request_final_usage = response.usage # continue for choice in response.choices: output_texts[choice.index] += choice.text if choice.finish_reason is not None and finish_reasons[choice.index] is None: finish_reasons[choice.index] = choice.finish_reason if choice.logprobs is not None: logprob_results[choice.index] = choice.logprobs assert all(finish_reason is not None for finish_reason in finish_reasons) return engine_base.wrap_completion_response( request_id=request_id, model=request.model, output_texts=output_texts, finish_reasons=finish_reasons, logprob_results=logprob_results, usage=request_final_usage, ) # 2. Set up app app = fastapi.FastAPI() app.add_middleware(CORSMiddleware) app.include_router(router_app) app.exception_handler(error_protocol.BadRequestError)(error_protocol.bad_request_error_handler) # 3. Run uvicorn.run(app, host=router_host, port=router_port, log_level="info")