94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
307 lines
11 KiB
Python
307 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from abc import ABC, abstractmethod
|
|
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union
|
|
|
|
import orjson
|
|
from fastapi import HTTPException, Request
|
|
from fastapi.responses import ORJSONResponse, StreamingResponse
|
|
|
|
from sglang.srt.entrypoints.openai.encoding_dsv32 import DS32EncodingError
|
|
from sglang.srt.entrypoints.openai.protocol import ErrorResponse, OpenAIServingRequest
|
|
from sglang.srt.managers.io_struct import EmbeddingReqInput, GenerateReqInput
|
|
from sglang.srt.observability.req_time_stats import monotonic_time
|
|
from sglang.srt.server_args import ServerArgs
|
|
|
|
if TYPE_CHECKING:
|
|
from sglang.srt.managers.tokenizer_manager import TokenizerManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Base class for specific endpoint handlers
|
|
class OpenAIServingBase(ABC):
|
|
"""Abstract base class for OpenAI endpoint handlers"""
|
|
|
|
def __init__(self, tokenizer_manager: TokenizerManager):
|
|
self.tokenizer_manager = tokenizer_manager
|
|
self.allowed_custom_labels = (
|
|
set(
|
|
self.tokenizer_manager.server_args.tokenizer_metrics_allowed_custom_labels
|
|
)
|
|
if isinstance(self.tokenizer_manager.server_args, ServerArgs)
|
|
and self.tokenizer_manager.server_args.tokenizer_metrics_allowed_custom_labels
|
|
else None
|
|
)
|
|
|
|
def _parse_model_parameter(self, model: str) -> Tuple[str, Optional[str]]:
|
|
"""Parse 'base-model:adapter-name' syntax to extract LoRA adapter.
|
|
|
|
Returns (base_model, adapter_name) or (model, None) if no colon present.
|
|
"""
|
|
if ":" not in model:
|
|
return model, None
|
|
|
|
# Split on first colon only to handle model paths with multiple colons
|
|
parts = model.split(":", 1)
|
|
base_model = parts[0].strip()
|
|
adapter_name = parts[1].strip() or None
|
|
|
|
return base_model, adapter_name
|
|
|
|
def _resolve_lora_path(
|
|
self,
|
|
request_model: str,
|
|
explicit_lora_path: Optional[Union[str, List[Optional[str]]]],
|
|
) -> Optional[Union[str, List[Optional[str]]]]:
|
|
"""Resolve LoRA adapter with priority: model parameter > explicit lora_path.
|
|
|
|
Returns adapter name or None. Supports both single values and lists (batches).
|
|
"""
|
|
_, adapter_from_model = self._parse_model_parameter(request_model)
|
|
|
|
# Model parameter adapter takes precedence
|
|
if adapter_from_model is not None:
|
|
return adapter_from_model
|
|
|
|
# Fall back to explicit lora_path
|
|
return explicit_lora_path
|
|
|
|
async def handle_request(
|
|
self, request: OpenAIServingRequest, raw_request: Request
|
|
) -> Union[Any, StreamingResponse, ErrorResponse]:
|
|
"""Handle the specific request type with common pattern
|
|
If you want to override this method, you should be careful to record the validation time.
|
|
"""
|
|
received_time = monotonic_time()
|
|
|
|
try:
|
|
# Validate request
|
|
error_msg = self._validate_request(request)
|
|
if error_msg:
|
|
return self.create_error_response(error_msg)
|
|
|
|
# Log the raw OpenAI request payload before conversion to tokenized form.
|
|
request_logger = self.tokenizer_manager.request_logger
|
|
if request_logger.log_requests and request_logger.log_requests_level >= 2:
|
|
request_logger.log_openai_received_request(request, request=raw_request)
|
|
|
|
# Convert to internal format
|
|
adapted_request, processed_request = self._convert_to_internal_request(
|
|
request, raw_request
|
|
)
|
|
|
|
if isinstance(adapted_request, (GenerateReqInput, EmbeddingReqInput)):
|
|
# Only set timing fields if adapted_request supports them
|
|
adapted_request.received_time = received_time
|
|
|
|
# Note(Xinyuan): raw_request below is only used for detecting the connection of the client
|
|
if hasattr(request, "stream") and request.stream:
|
|
return await self._handle_streaming_request(
|
|
adapted_request, processed_request, raw_request
|
|
)
|
|
else:
|
|
return await self._handle_non_streaming_request(
|
|
adapted_request, processed_request, raw_request
|
|
)
|
|
except HTTPException as e:
|
|
return self.create_error_response(
|
|
message=e.detail, err_type=str(e.status_code), status_code=e.status_code
|
|
)
|
|
except ValueError as e:
|
|
return self.create_error_response(
|
|
message=str(e),
|
|
err_type="BadRequest",
|
|
status_code=400,
|
|
)
|
|
except DS32EncodingError as e:
|
|
logger.info(f"DS32EncodingError: {e}")
|
|
return self.create_error_response(
|
|
message=str(e),
|
|
err_type="BadRequest",
|
|
status_code=400,
|
|
)
|
|
except Exception as e:
|
|
logger.exception(f"Error in request: {e}")
|
|
return self.create_error_response(
|
|
message=f"Internal server error: {str(e)}",
|
|
err_type="InternalServerError",
|
|
status_code=500,
|
|
)
|
|
|
|
@abstractmethod
|
|
def _request_id_prefix(self) -> str:
|
|
"""Generate request ID based on request type"""
|
|
pass
|
|
|
|
def _generate_request_id_base(self, request: OpenAIServingRequest) -> Optional[str]:
|
|
"""Generate request ID based on request type"""
|
|
return None
|
|
|
|
# TODO(chang): the rid is used in io_strcut check and often violates `The rid should be a list` AssertionError
|
|
# Temporarily return None in this function until the rid logic is clear.
|
|
if rid := getattr(request, "rid", None):
|
|
return rid
|
|
|
|
return f"{self._request_id_prefix()}{uuid.uuid4().hex}"
|
|
|
|
def _compute_extra_key(self, request: OpenAIServingRequest) -> Optional[str]:
|
|
"""Compute the final extra_key by concatenating cache_salt and extra_key if both are provided."""
|
|
parts = []
|
|
for key in ["cache_salt", "extra_key"]:
|
|
value = getattr(request, key, None)
|
|
if value:
|
|
if not isinstance(value, str):
|
|
raise TypeError(
|
|
f"Value of {key} must be a string, but got {type(value).__name__}"
|
|
)
|
|
parts.append(value)
|
|
return "".join(parts) if parts else None
|
|
|
|
@abstractmethod
|
|
def _convert_to_internal_request(
|
|
self,
|
|
request: OpenAIServingRequest,
|
|
raw_request: Request = None,
|
|
) -> tuple[GenerateReqInput, OpenAIServingRequest]:
|
|
"""Convert OpenAI request to internal format"""
|
|
pass
|
|
|
|
async def _handle_streaming_request(
|
|
self,
|
|
adapted_request: GenerateReqInput,
|
|
request: OpenAIServingRequest,
|
|
raw_request: Request,
|
|
) -> Union[StreamingResponse, ErrorResponse, ORJSONResponse]:
|
|
"""Handle streaming request
|
|
|
|
Override this method in child classes that support streaming requests.
|
|
"""
|
|
return self.create_error_response(
|
|
message=f"{self.__class__.__name__} does not support streaming requests",
|
|
err_type="NotImplementedError",
|
|
status_code=501,
|
|
)
|
|
|
|
async def _handle_non_streaming_request(
|
|
self,
|
|
adapted_request: GenerateReqInput,
|
|
request: OpenAIServingRequest,
|
|
raw_request: Request,
|
|
) -> Union[Any, ErrorResponse, ORJSONResponse]:
|
|
"""Handle non-streaming request
|
|
|
|
Override this method in child classes that support non-streaming requests.
|
|
"""
|
|
return self.create_error_response(
|
|
message=f"{self.__class__.__name__} does not support non-streaming requests",
|
|
err_type="NotImplementedError",
|
|
status_code=501,
|
|
)
|
|
|
|
def _validate_request(self, _: OpenAIServingRequest) -> Optional[str]:
|
|
"""Validate request"""
|
|
pass
|
|
|
|
def create_error_response(
|
|
self,
|
|
message: str,
|
|
err_type: str = "BadRequestError",
|
|
status_code: int = 400,
|
|
param: Optional[str] = None,
|
|
) -> ORJSONResponse:
|
|
"""Create an error response"""
|
|
# TODO: remove fastapi dependency in openai and move response handling to the entrypoint
|
|
error = ErrorResponse(
|
|
object="error",
|
|
message=message,
|
|
type=err_type,
|
|
param=param,
|
|
code=status_code,
|
|
)
|
|
return ORJSONResponse(content=error.model_dump(), status_code=status_code)
|
|
|
|
def create_streaming_error_response(
|
|
self,
|
|
message: str,
|
|
err_type: str = "BadRequestError",
|
|
status_code: int = 400,
|
|
) -> str:
|
|
"""Create a streaming error response"""
|
|
error = ErrorResponse(
|
|
object="error",
|
|
message=message,
|
|
type=err_type,
|
|
param=None,
|
|
code=status_code,
|
|
)
|
|
return json.dumps({"error": error.model_dump()})
|
|
|
|
def extract_custom_labels(self, raw_request):
|
|
if (
|
|
not self.allowed_custom_labels
|
|
or not self.tokenizer_manager.server_args.tokenizer_metrics_custom_labels_header
|
|
):
|
|
return None
|
|
|
|
custom_labels = None
|
|
header = (
|
|
self.tokenizer_manager.server_args.tokenizer_metrics_custom_labels_header
|
|
)
|
|
try:
|
|
raw_labels = (
|
|
orjson.loads(raw_request.headers.get(header))
|
|
if raw_request and raw_request.headers.get(header)
|
|
else None
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
logger.exception(f"Error in request: {e}")
|
|
raw_labels = None
|
|
|
|
if isinstance(raw_labels, dict):
|
|
custom_labels = {
|
|
label: value
|
|
for label, value in raw_labels.items()
|
|
if label in self.allowed_custom_labels
|
|
}
|
|
return custom_labels
|
|
|
|
def extract_routing_key(self, raw_request):
|
|
if raw_request is None:
|
|
return None
|
|
return raw_request.headers.get("x-smg-routing-key")
|
|
|
|
def extract_routed_dp_rank_from_header(
|
|
self, raw_request: Request, body_routed_dp_rank: Optional[int] = None
|
|
) -> Optional[int]:
|
|
"""Extract routed_dp_rank from HTTP header, with higher priority than routed_dp_rank in body.
|
|
|
|
Header name: X-Data-Parallel-Rank (case-insensitive in HTTP/1.1/2)
|
|
"""
|
|
if raw_request is None:
|
|
return body_routed_dp_rank
|
|
|
|
header_value = raw_request.headers.get("x-data-parallel-rank")
|
|
if header_value is not None:
|
|
try:
|
|
header_dp_rank = int(header_value)
|
|
if (
|
|
body_routed_dp_rank is not None
|
|
and header_dp_rank != body_routed_dp_rank
|
|
):
|
|
logger.debug(
|
|
f"X-Data-Parallel-Rank header ({header_dp_rank}) overrides "
|
|
f"body routed_dp_rank ({body_routed_dp_rank})"
|
|
)
|
|
return header_dp_rank
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Invalid X-Data-Parallel-Rank header: must be an integer, got '{header_value}'",
|
|
)
|
|
|
|
return body_routed_dp_rank
|