본문으로 건너뛰기

[SGLang] Anthropic/Ollama 호환 API: 멀티 프로토콜 LLM 서빙

들어가며

LLM 서빙 프레임워크를 선택할 때 가장 큰 고려사항 중 하나는 클라이언트 호환성이다. 기존에 OpenAI SDK로 개발된 애플리케이션은 OpenAI 호환 API만 있으면 되지만, Anthropic SDK나 Ollama CLI를 사용하는 프로젝트도 많다. SGLang은 이 문제를 프로토콜 변환 레이어로 해결한다. OpenAI ChatCompletion을 핵심 엔진으로 두고, Anthropic Messages API와 Ollama API를 변환 계층으로 감싸는 구조다.

이 글에서는 python/sglang/srt/entrypoints/ 하위의 Anthropic 레이어, Ollama 레이어, 그리고 Smart Router의 실제 코드를 분석한다.

멀티 프로토콜 구조도

SGLang의 멀티 프로토콜 아키텍처는 다음과 같은 계층 구조를 따른다.

Client (Anthropic SDK / Ollama CLI / OpenAI SDK)
        │
        ▼
┌──────────────────────────────────┐
│   FastAPI Entrypoints            │
│  ┌──────────┬──────────┬───────┐ │
│  │Anthropic │  Ollama  │OpenAI │ │
│  │ /v1/     │  /api/   │ /v1/  │ │
│  │messages  │  chat    │ chat/ │ │
│  └────┬─────┴────┬─────┴───┬───┘ │
│       │          │         │     │
│       ▼          ▼         │     │
│  ┌─────────┐ ┌────────┐   │     │
│  │Anthropic│ │Ollama  │   │     │
│  │Serving  │ │Serving │   │     │
│  └────┬────┘ └───┬────┘   │     │
│       │          │         │     │
│       ▼          ▼         ▼     │
│   ┌──────────────────────────┐   │
│   │  OpenAI Serving Chat     │   │
│   │  (핵심 추론 엔진)          │   │
│   └──────────────────────────┘   │
└──────────────────────────────────┘

핵심 원칙은 명확하다. 모든 요청은 최종적으로 OpenAI ChatCompletion 형식으로 변환되어 처리된다. Anthropic과 Ollama 레이어는 순수한 프로토콜 변환기 역할만 한다.

핵심 코드 분석

Anthropic 레이어: 요청 변환

python/sglang/srt/entrypoints/anthropic/serving.pyAnthropicServing 클래스는 Anthropic Messages API 요청을 OpenAI 형식으로 변환한다. 가장 핵심적인 메서드는 _convert_to_chat_completion_request다.

# anthropic/serving.py - 요청 변환의 핵심
def _convert_to_chat_completion_request(
    self, anthropic_request: AnthropicMessagesRequest
) -> ChatCompletionRequest:
    openai_messages = []

    # Anthropic의 system은 별도 필드, OpenAI는 메시지 배열의 첫 번째 항목
    if anthropic_request.system:
        if isinstance(anthropic_request.system, str):
            openai_messages.append(
                {"role": "system", "content": anthropic_request.system}
            )

    # 메시지 변환: content block 구조를 OpenAI 형식으로 평탄화
    for msg in anthropic_request.messages:
        if isinstance(msg.content, str):
            openai_messages.append({"role": msg.role, "content": msg.content})
            continue
        # Complex content with blocks ...

Anthropic API에서 system은 요청 본문의 독립 필드지만, OpenAI에서는 messages 배열의 role: "system" 메시지로 들어간다. 이 차이를 변환기가 자동으로 처리한다.

Anthropic 레이어: 응답 변환과 Stop Reason 매핑

응답 변환에서 주목할 부분은 stop reason 매핑이다. 두 API는 종료 이유를 다른 이름으로 표현한다.

# anthropic/serving.py - 종료 이유 매핑
STOP_REASON_MAP = {
    "stop": "end_turn",
    "length": "max_tokens",
    "tool_calls": "tool_use",
}

OpenAI의 "stop"은 Anthropic에서 "end_turn", "length""max_tokens", "tool_calls""tool_use"가 된다. 단순해 보이지만, 클라이언트 SDK가 이 값에 의존하여 후속 동작을 결정하므로 정확한 매핑이 필수적이다.

Anthropic 레이어: Tool Choice 변환

Tool use 프로토콜에서도 두 API 사이의 미묘한 차이가 있다. Anthropic의 "any" (아무 tool이든 반드시 호출)는 OpenAI의 "required"에 대응한다.

# anthropic/serving.py - tool_choice 변환
if anthropic_request.tool_choice.type == "any":
    chat_request.tool_choice = "required"
elif anthropic_request.tool_choice.type == "tool":
    chat_request.tool_choice = ToolChoice(
        type="function",
        function=ToolChoiceFuncName(
            name=anthropic_request.tool_choice.name
        ),
    )

Ollama 레이어: 직접 엔진 호출

Ollama 레이어는 Anthropic과 다른 전략을 취한다. OpenAI Serving Chat을 경유하지 않고, tokenizer_manager에 직접 GenerateReqInput을 전달한다.

# ollama/serving.py - 직접 엔진 호출 방식
async def handle_chat(
    self, request: OllamaChatRequest, raw_request: Request
) -> Union[OllamaChatResponse, StreamingResponse]:
    # 직접 chat template 적용
    prompt_ids = self.tokenizer_manager.tokenizer.apply_chat_template(
        messages, tokenize=True, add_generation_prompt=True,
    )

    sampling_params = self._convert_options_to_sampling_params(request.options)

    # OpenAI Serving Chat을 거치지 않고 직접 GenerateReqInput 생성
    gen_request = GenerateReqInput(
        input_ids=prompt_ids,
        sampling_params=sampling_params,
        stream=request.stream,
    )

이 설계는 Ollama API의 단순한 특성을 반영한다. Ollama는 tool use, structured output 같은 복잡한 기능보다 빠른 로컬 추론에 초점을 맞추므로, OpenAI 레이어를 거칠 필요가 없다.

Ollama 레이어: 파라미터 매핑

Ollama의 options 딕셔너리는 SGLang의 sampling params로 변환된다. 특히 num_predictmax_new_tokens로 매핑되며, 지정하지 않으면 기본값 2048이 적용된다.

# ollama/serving.py - Ollama → SGLang 파라미터 매핑
param_mapping = {
    "temperature": "temperature",
    "top_p": "top_p",
    "top_k": "top_k",
    "num_predict": "max_new_tokens",
    "stop": "stop",
    "presence_penalty": "presence_penalty",
    "frequency_penalty": "frequency_penalty",
    "seed": "seed",
}

# Ollama 사용자는 긴 응답을 기대하므로 SGLang 기본값(128)보다 높게 설정
if "max_new_tokens" not in sampling_params:
    sampling_params["max_new_tokens"] = 2048

Smart Router: LLM Judge 기반 라우팅

python/sglang/srt/entrypoints/ollama/smart_router.pySmartRouter는 요청의 복잡도를 판단하여 로컬 Ollama와 원격 SGLang 서버 사이에서 라우팅한다. 핵심은 LLM 자체를 분류기로 사용한다는 점이다.

# ollama/smart_router.py - LLM Judge 분류
CLASSIFICATION_PROMPT = """You are a task classifier. Classify the following user request into one of two categories.

Categories:
- SIMPLE: Quick responses, greetings, factual questions, definitions, translations, basic Q&A
- COMPLEX: Tasks requiring deep reasoning, multi-step analysis, long explanations, creative writing, detailed research

Reply with ONLY one word: either SIMPLE or COMPLEX.

User request: "{prompt}"

Category:"""

SIMPLE로 분류되면 로컬 Ollama(빠르지만 작은 모델), COMPLEX면 원격 SGLang(느리지만 강력한 모델)으로 라우팅된다. 한쪽이 실패하면 자동으로 다른 쪽으로 fallback하는 안전장치도 구현되어 있다.

# ollama/smart_router.py - Fallback 메커니즘
except Exception as e:
    fallback_client = (
        self.remote_client if not use_remote else self.local_client
    )
    fallback_model = self.remote_model if not use_remote else self.local_model
    response = fallback_client.chat(model=fallback_model, messages=messages)

프로토콜 간 차이 비교 테이블

세 가지 API 프로토콜의 주요 차이를 정리하면 다음과 같다.

항목 OpenAI Anthropic Ollama
엔드포인트 /v1/chat/completions /v1/messages /api/chat, /api/generate
System 메시지 messages 배열에 role: "system" 별도 system 필드 system 필드 (generate)
응답 종료 이유 stop, length, tool_calls end_turn, max_tokens, tool_use stop (단순)
Streaming 형식 SSE (data: {...}) SSE (typed events) NDJSON ({...}\n)
Max Tokens 필드 max_tokens (선택) max_tokens (필수) options.num_predict
Tool Choice "반드시 호출" "required" "any" 미지원
Content 구조 string 또는 array ContentBlock 배열 string
Token 카운팅 응답의 usage 필드 /v1/messages/count_tokens 별도 엔드포인트 응답의 eval_count
SGLang 내부 경로 직접 처리 OpenAI Serving Chat 경유 tokenizer_manager 직접 호출

Streaming 형식의 차이가 특히 주목할 만하다. OpenAI는 data: {json} 형태의 Server-Sent Events를 사용하고, Anthropic은 여기에 event: {type} 행을 추가하여 이벤트 유형을 명시한다. 반면 Ollama는 SSE가 아닌 Newline-Delimited JSON(NDJSON)을 사용하여 각 줄이 독립적인 JSON 객체다.

관련 포스트

  • SGLang 시리즈의 다른 분석 글도 참고할 수 있다.

참고

댓글

관련 포스트

SGLang 의 다른글