[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.py의 AnthropicServing 클래스는 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_predict가 max_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.py의 SmartRouter는 요청의 복잡도를 판단하여 로컬 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 GitHub Repository
- Anthropic Messages API Documentation
- Ollama API Documentation
python/sglang/srt/entrypoints/anthropic/serving.pypython/sglang/srt/entrypoints/anthropic/protocol.pypython/sglang/srt/entrypoints/ollama/serving.pypython/sglang/srt/entrypoints/ollama/protocol.pypython/sglang/srt/entrypoints/ollama/smart_router.py
관련 포스트
SGLang 의 다른글
- 이전글 [SGLang] OpenAI 호환 API: Chat, Completions, Embedding 엔드포인트 구현
- 현재글 : [SGLang] Anthropic/Ollama 호환 API: 멀티 프로토콜 LLM 서빙
- 다음글 [SGLang] gRPC 서버: 분산 추론을 위한 고성능 통신 계층
댓글