본문으로 건너뛰기

[SGLang] OpenAI 호환 API: Chat, Completions, Embedding 엔드포인트 구현

들어가며

SGLang은 자체 /generate API 외에 OpenAI 호환 API를 제공한다. /v1/chat/completions, /v1/completions, /v1/embeddings 세 엔드포인트를 지원하므로, 기존 OpenAI SDK 코드를 그대로 사용해 SGLang 서버로 요청을 보낼 수 있다.

이 글에서는 python/sglang/srt/entrypoints/openai/ 디렉토리의 핵심 파일들을 분석한다. OpenAI 형식의 요청이 SGLang 내부의 GenerateReqInput으로 변환되는 과정, 스트리밍 응답 구현, 그리고 SGLang만의 확장 파라미터를 살펴본다.

OpenAI API 호환 구조도

요청이 들어오면 각 엔드포인트 핸들러가 OpenAI 형식을 SGLang 내부 형식으로 변환한 뒤 TokenizerManager에 전달한다.

Client (OpenAI SDK)
    |
    |  POST /v1/chat/completions
    |  POST /v1/completions
    |  POST /v1/embeddings
    v
+-------------------------------------------+
|  FastAPI Router                           |
+-------------------------------------------+
    |               |               |
    v               v               v
+----------+  +-----------+  +----------+
| Serving  |  | Serving   |  | Serving  |
| Chat     |  | Completion|  | Embedding|
+----------+  +-----------+  +----------+
    |               |               |
    |   _convert_to_internal_request()
    v               v               v
+-------------------------------------------+
|  protocol.py (Pydantic models)            |
|  ChatCompletionRequest -> GenerateReqInput|
|  CompletionRequest     -> GenerateReqInput|
|  EmbeddingRequest      -> EmbeddingReqInput|
+-------------------------------------------+
    |
    v
+-------------------------------------------+
|  TokenizerManager.generate_request()      |
|  -> Scheduler -> Model Execution          |
+-------------------------------------------+
    |
    v
  Response (streaming SSE / JSON)

핵심 코드 분석

OpenAIServingBase: 공통 추상 클래스

python/sglang/srt/entrypoints/openai/serving_base.py에 정의된 OpenAIServingBase는 세 엔드포인트의 공통 로직을 담은 추상 클래스다. Template Method 패턴으로 요청 처리 흐름을 정의한다.

class OpenAIServingBase(ABC):
    async def handle_request(
        self, request: OpenAIServingRequest, raw_request: Request
    ) -> Union[Any, StreamingResponse, ErrorResponse]:
        received_time = monotonic_time()
        try:
            error_msg = self._validate_request(request)
            if error_msg:
                return self.create_error_response(error_msg)

            adapted_request, processed_request = self._convert_to_internal_request(
                request, raw_request
            )

            if isinstance(adapted_request, (GenerateReqInput, EmbeddingReqInput)):
                adapted_request.received_time = received_time

            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
            )

핵심은 _validate_request -> _convert_to_internal_request -> streaming/non-streaming 분기의 3단계 파이프라인이다. 각 서브클래스는 이 추상 메서드를 구현한다.

LoRA adapter 지원도 base에서 처리한다. model 파라미터에 base-model:adapter-name 형식을 사용하면 자동으로 파싱된다.

def _parse_model_parameter(self, model: str) -> Tuple[str, Optional[str]]:
    if ":" not in model:
        return model, None
    parts = model.split(":", 1)
    base_model = parts[0].strip()
    adapter_name = parts[1].strip() or None
    return base_model, adapter_name

ServingChat: Chat Completions 엔드포인트

python/sglang/srt/entrypoints/openai/serving_chat.pyOpenAIServingChat/v1/chat/completions를 담당한다. 가장 복잡한 핸들러로, chat template 적용, tool calling, reasoning mode, multimodal 입력을 모두 처리한다.

_convert_to_internal_request 메서드에서 OpenAI 형식 메시지를 내부 형식으로 변환한다.

def _convert_to_internal_request(
    self, request: ChatCompletionRequest, raw_request: Request = None,
) -> tuple[GenerateReqInput, ChatCompletionRequest]:
    is_multimodal = self.tokenizer_manager.model_config.is_multimodal
    processed_messages = self._process_messages(request, is_multimodal)

    sampling_params = request.to_sampling_params(
        stop=processed_messages.stop,
        model_generation_config=self.default_sampling_params,
        tool_call_constraint=processed_messages.tool_call_constraint,
    )

    adapted_request = GenerateReqInput(
        **prompt_kwargs,
        image_data=processed_messages.image_data,
        video_data=processed_messages.video_data,
        audio_data=processed_messages.audio_data,
        sampling_params=sampling_params,
        return_logprob=request.logprobs,
        stream=request.stream,
        # ... 추가 파라미터
    )
    return adapted_request, request

Chat template 적용은 두 경로로 나뉜다. HuggingFace tokenizer의 Jinja template이 있으면 apply_chat_template()을 호출하고, 없으면 SGLang 내장 conversation template을 사용한다. DeepSeek V3의 경우 별도의 DSv32 encoding 경로를 탄다.

continue_final_message 기능은 SGLang 확장 기능이다. 마지막 메시지가 assistant이면 해당 내용을 prefix로 분리하여 이어서 생성하도록 한다.

ServingCompletions: Text Completions 엔드포인트

python/sglang/srt/entrypoints/openai/serving_completions.pyOpenAIServingCompletion/v1/completions를 담당한다. Chat과 달리 chat template 적용 없이 raw prompt를 그대로 처리한다.

스트리밍 응답 구현에서 주목할 점은 첫 번째 chunk를 미리 소비(kick-start)하여 validation 에러를 HTTP 200 전에 감지하는 패턴이다.

async def _handle_streaming_request(self, adapted_request, request, raw_request):
    generator = self._generate_completion_stream(adapted_request, request, raw_request)
    try:
        first_chunk = await generator.__anext__()
    except ValueError as e:
        return self.create_error_response(str(e))

    async def prepend_first_chunk():
        yield first_chunk
        async for chunk in generator:
            yield chunk

    return StreamingResponse(
        prepend_first_chunk(),
        media_type="text/event-stream",
        background=self.tokenizer_manager.create_abort_task(adapted_request),
    )

이 패턴은 Chat 핸들러에서도 동일하게 사용된다. 스트리밍 응답이 시작된 뒤에는 HTTP 상태 코드를 변경할 수 없으므로, 첫 chunk를 먼저 생성해보고 에러가 발생하면 일반 에러 응답을 반환한다.

ServingEmbedding: Embeddings 엔드포인트

python/sglang/srt/entrypoints/openai/serving_embedding.pyOpenAIServingEmbedding/v1/embeddings를 담당한다. 다른 핸들러와 달리 EmbeddingReqInput으로 변환하며 스트리밍을 지원하지 않는다.

class OpenAIServingEmbedding(OpenAIServingBase):
    def _convert_to_internal_request(
        self, request: EmbeddingRequest, raw_request: Request = None,
    ) -> tuple[EmbeddingReqInput, EmbeddingRequest]:
        prompt = request.input
        if isinstance(prompt, str):
            prompt_kwargs = {"text": prompt}
        elif isinstance(prompt, list):
            if len(prompt) > 0 and isinstance(prompt[0], str):
                prompt_kwargs = {"text": prompt}
            elif len(prompt) > 0 and isinstance(prompt[0], MultimodalEmbeddingInput):
                # Multimodal embedding 처리...
            else:
                prompt_kwargs = {"input_ids": prompt}

        adapted_request = EmbeddingReqInput(
            **prompt_kwargs,
            dimensions=request.dimensions,
            lora_path=lora_path,
            embed_override_token_id=request.embed_override_token_id,
            embed_overrides=embed_overrides,
        )
        return adapted_request, request

Multimodal embedding 입력도 지원한다. MultimodalEmbeddingInput으로 text, image, video를 함께 전달할 수 있으며, chat template이 설정되어 있으면 대화 형식으로 변환한 뒤 임베딩을 추출한다.

프로토콜 변환: protocol.py

python/sglang/srt/entrypoints/openai/protocol.py에는 모든 요청/응답의 Pydantic 모델이 정의되어 있다. ChatCompletionRequestto_sampling_params() 메서드가 핵심이다.

def to_sampling_params(self, stop, model_generation_config, tool_call_constraint=None):
    def get_param(param_name: str):
        value = getattr(self, param_name)
        if value is None:
            return model_generation_config.get(
                param_name, self._DEFAULT_SAMPLING_PARAMS[param_name]
            )
        return value

    sampling_params = {
        "temperature": get_param("temperature"),
        "max_new_tokens": self.max_completion_tokens or self.max_tokens,
        "top_p": get_param("top_p"),
        "top_k": get_param("top_k"),
        "min_p": get_param("min_p"),
        "repetition_penalty": get_param("repetition_penalty"),
        # ...
    }

파라미터 우선순위는 사용자 지정값 > 모델 generation_config > OpenAI 기본값 순이다. 모델마다 다른 기본 sampling 설정을 generation_config에서 읽어 적용할 수 있다.

OpenAIServingRequest 타입은 세 요청 타입의 Union으로 정의된다:

OpenAIServingRequest = Union[
    ChatCompletionRequest,
    CompletionRequest,
    EmbeddingRequest,
    # ...
]

기존 OpenAI API 대비 확장점

SGLang은 OpenAI API를 완전히 호환하면서 독자적인 파라미터를 추가했다. 아래 표는 주요 확장 기능을 정리한 것이다.

기능 OpenAI 표준 SGLang 확장
Constrained decoding response_format (json_schema, json_object) regex, ebnf, structural_tag 추가 지원
Sampling temperature, top_p top_k, min_p, repetition_penalty 추가
Stop 조건 stop (문자열/리스트) stop_token_ids, stop_regex, no_stop_trim, ignore_eos 추가
LoRA 미지원 model 필드에 base:adapter 구문 또는 lora_path 파라미터
Reasoning reasoning_effort separate_reasoning, stream_reasoning, chat_template_kwargs
Data Parallel 라우팅 미지원 routed_dp_rank, X-Data-Parallel-Rank 헤더
Prefill-Decode 분리 미지원 bootstrap_host/port/room, disagg_prefill_dp_rank
캐시 제어 미지원 cache_salt, extra_key
Hidden states 미지원 return_hidden_states
Expert 라우팅 정보 미지원 return_routed_experts
어시스턴트 이어쓰기 미지원 continue_final_message
Embedding override 미지원 embed_override_token_id, embed_overrides
요청 우선순위 미지원 priority

이 확장 파라미터들은 sglext 필드로 응답에도 반영된다. SglExt 모델은 routed_experts, cached_tokens_details 등 SGLang 전용 응답 데이터를 담으며, None인 필드는 직렬화에서 자동 제외되어 표준 OpenAI 응답과 호환성을 유지한다.

관련 포스트

참고

댓글

관련 포스트

SGLang 의 다른글