Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Waiting to run
Windows CI / Windows (push) Waiting to run
Build Docs / Deploy Docs (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

126 lines
5.1 KiB
Python

"""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")