Recipes

OpenLLM과 vLLM을 활용한 자체 호스팅 OpenAI 호환 LLM API

README 링크에서 발견 · 2 days ago

Research & KnowledgeSoftware & DevOps

앱 테스트를 위해 Qwen2.5 모델을 로컬에서 OpenAI 호환 엔드포인트로 서빙했습니다.

What they did

개발자는 내부 채팅 어시스턴트를 프로토타이핑하면서 토큰당 API 청구서를 더 이상 지불하고 싶지 않아서, 호스팅 제공업체를 호출하는 대신 오픈소스 모델을 셀프 호스팅했다. 그들은 pip로 OpenLLM을 설치하고, `openllm hello`를 실행해 CLI가 작동하는지 확인한 다음, `openllm serve qwen2.5:7b`로 Qwen2.5 7B 서버를 시작했는데, 이는 24G GPU 한 장에 들어갔다. OpenLLM은 자체적으로 가중치를 저장하지 않기 때문에, 게이트된 모델 다운로드가 성공하도록 먼저 HF_TOKEN 환경 변수를 설정했다. 서버는 OpenAI 호환 REST API와 내장 채팅 UI와 함께 올라왔고, 그들은 다른 것을 연결하기 전에 응답을 sanity-check하는 데 이것을 사용했다. 그들은 기존 앱의 OpenAI 클라이언트를 로컬 base URL로 향하게 하면서 base_url과 api_key 값만 변경했기 때문에 애플리케이션 코드는 바뀌지 않았다. 팀원들을 위해 설정을 재현 가능하게 만들기 위해, 그들은 서버를 Docker 이미지로 감싸고 GPU 요구 사항과 토큰 설정을 repo README에 문서화했다. 그들은 또한 내부적으로 추론 백엔드로 vLLM을 사용한다는 점을 언급했는데, 이것이 적당한 하드웨어에서도 배치 요청의 처리량이 합리적이었던 이유다.

How it went

로컬 엔드포인트는 처음부터 끝까지 잘 작동했다. 앱의 기존 OpenAI 클라이언트 코드가 셀프 호스팅 모델에 대해 변경 없이 실행되었고, 내장 채팅 UI 덕분에 프롬프트를 빠르게 테스트하기가 쉬웠다. Docker 패키징은 팀원들에게 반복 가능한 원커맨드 시작 방법을 제공했다. 7B 모델의 응답 품질은 미묘한 작업에서는 호스팅된 프런티어 모델보다 눈에 띄게 낮았지만, 프로토타입의 분류 및 초안 작성 흐름에는 충분했다.

Worth knowing

7B 모델 기준 24G VRAM을 갖춘 GPU와 gated weights를 위한 Hugging Face 토큰이 필요합니다. OpenLLM은 모델 가중치를 호스팅하거나 캐싱해 주지 않습니다. 더 큰 모델은 멀티 GPU 구성이 필요합니다. README 발췌에는 인증, rate limiting, 프로덕션 하드닝이 다뤄지지 않으므로, 엔드포인트를 localhost 외부에 노출하기 전에 별도로 추가해야 합니다.

Wiring

작업 순서: pip로 OpenLLM을 설치하고, HF_TOKEN을 export해 gated 모델 가중치를 다운로드할 수 있게 한 다음, `openllm serve <model>:<size>`를 실행해 로컬 포트에서 OpenAI 호환 API와 채팅 UI를 띄웁니다. 앱의 OpenAI 클라이언트는 base_url을 해당 로컬 포트로, api_key는 플레이스홀더로 설정되어 있어 기존 chat completion 호출이 셀프 호스팅 모델로 라우팅됩니다. 배포의 경우 Dockerfile이 openllm을 설치하고 동일한 serve 명령을 시작하며, 런타임에 GPU 접근이 전달됩니다. vLLM은 OpenLLM 뒤에서 추론 백엔드 역할을 하며 배칭과 GPU 메모리를 처리합니다. 설정은 코드가 아닌 환경 변수(HF_TOKEN, base URL, 모델 이름)에 저장됩니다.

Try it yourself

# Self-Hosted OpenAI-Compatible LLM Endpoint with OpenLLM

## 1. Dependencies

**`requirements.txt`** (append):

```txt
openllm>=0.6.0
openai>=1.30.0
```

**`pyproject.toml`** (if you use one, add to `[project]` dependencies):

```toml
dependencies = [
    "openllm>=0.6.0",
    "openai>=1.30.0",
]
```

Install with:

```bash
pip install openllm openai
```

## 2. `scripts/serve_llm.sh`

```bash
#!/usr/bin/env bash
# Serve a Qwen2.5 7B model via OpenLLM's OpenAI-compatible endpoint.
# Requires: HF_TOKEN exported in the environment (never hardcode it).
# Requires: a GPU with ~24G VRAM for qwen2.5:7b.

set -euo pipefail

if [[ -z "${HF_TOKEN:-}" ]]; then
  echo "ERROR: HF_TOKEN is not set. Export it before running this script." >&2
  echo "  export HF_TOKEN=hf_xxx" >&2
  exit 1
fi

export HF_TOKEN

# Optional: override the model via MODEL env var, e.g. MODEL=qwen2.5:14b
MODEL="${MODEL:-qwen2.5:7b}"

exec openllm serve "${MODEL}"
```

Make it executable:

```bash
chmod +x scripts/serve_llm.sh
```

## 3. Python client module

**`llm_client.py`**:

```python
"""OpenAI-compatible chat client.

Uses a self-hosted OpenLLM endpoint when OPENLLM_BASE_URL is set,
otherwise falls back to the hosted OpenAI API.
"""

from __future__ import annotations

import os
from typing import Any

from openai import OpenAI


def _build_client() -> tuple[OpenAI, str]:
    base_url = os.environ.get("OPENLLM_BASE_URL")
    if base_url:
        api_key = os.environ.get("OPENLLM_API_KEY", "not-needed")
        return OpenAI(base_url=base_url, api_key=api_key), "openllm"

    # Fallback: hosted OpenAI API. Requires OPENAI_API_KEY.
    return OpenAI(), "openai"


def chat(
    messages: list[dict[str, Any]],
    model: str | None = None,
    **kwargs: Any,
) -> str:
    """Send a chat completion request and return the assistant message text."""
    client, backend = _build_client()

    if model is None:
        if backend == "openllm":
            model = os.environ.get("OPENLLM_MODEL", "qwen2.5:7b")
        else:
            model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")

    response = client.chat.completions.create(
        model=model,
        messages=messages,
        **kwargs,
    )
    return response.choices[0].message.content or ""


if __name__ == "__main__":
    print(chat([{"role": "user", "content": "Say hello in one sentence."}]))
```

Usage:

```bash
# Self-hosted
export OPENLLM_BASE_URL=http://localhost:3000/v1
export OPENLLM_API_KEY=local-dev-key   # any non-empty string
python llm_client.py

# Hosted fallback
unset OPENLLM_BASE_URL
export OPENAI_API_KEY=sk-...
python llm_client.py
```

## 4. `Dockerfile`

```dockerfile
# OpenLLM server image.
#
# GPU REQUIREMENT: qwen2.5:7b needs roughly 24G of GPU VRAM.
# Run with: docker run --gpus all -p 3000:3000 -e HF_TOKEN=$HF_TOKEN <image>
FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

RUN pip install --upgrade pip && \
    pip install "openllm>=0.6.0"

# HF_TOKEN is injected at runtime via -e HF_TOKEN=... (never baked into the image).
ENV MODEL=qwen2.5:7b

EXPOSE 3000

CMD ["sh", "-c", "openllm serve ${MODEL}"]
```

Build and run:

```bash
docker build -t openllm-server .
docker run --gpus all -p 3000:3000 -e HF_TOKEN="$HF_TOKEN" openllm-server
```

## 5. README section

````markdown
## Self-Hosted LLM Endpoint (OpenLLM)

This repo can serve an OpenAI-compatible LLM endpoint locally using
[OpenLLM](https://github.com/bentoml/OpenLLM). The Python client in
`llm_client.py` talks to it automatically when `OPENLLM_BASE_URL` is set,
and falls back to the hosted OpenAI API otherwise.

### Prerequisites

- Python 3.10+
- A CUDA-capable GPU. **`qwen2.5:7b` requires ~24G of GPU VRAM.**
  Smaller models (e.g. `qwen2.5:1.5b`) fit on smaller cards.
- A Hugging Face account and access token.

### 1. Install

```bash
pip install openllm openai
```

### 2. Set up `HF_TOKEN`

Create a token at <https://huggingface.co/settings/tokens> and export it
in your shell. **Never commit tokens to the repo.**

```bash
export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxx
```

The serve script and Dockerfile both read `HF_TOKEN` from the environment
at runtime; nothing is hardcoded.

### 3. Start the server

```bash
./scripts/serve_llm.sh
```

This runs `openllm serve qwen2.5:7b` and exposes an OpenAI-compatible API
at `http://localhost:3000/v1`.

Or with Docker:

```bash
docker build -t openllm-server .
docker run --gpus all -p 3000:3000 -e HF_TOKEN="$HF_TOKEN" openllm-server
```

### 4. Point the client at it

```bash
export OPENLLM_BASE_URL=http://localhost:3000/v1
export OPENLLM_API_KEY=local-dev-key   # any non-empty string
python llm_client.py
```

If `OPENLLM_BASE_URL` is unset, `llm_client.py` falls back to the hosted
OpenAI API and uses `OPENAI_API_KEY`.

### 5. Switching models

Pick any model supported by OpenLLM. Two ways:

- **Script:** set the `MODEL` env var before running the script:

  ```bash
  MODEL=qwen2.5:14b ./scripts/serve_llm.sh
  ```

- **Docker:** override the `MODEL` env var at runtime:

  ```bash
  docker run --gpus all -p 3000:3000 \
    -e HF_TOKEN="$HF_TOKEN" \
    -e MODEL=qwen2.5:14b \
    openllm-server
  ```

Then update the client's model name if needed:

```bash
export OPENLLM_MODEL=qwen2.5:14b
```

Check VRAM requirements for larger models before switching — 14B and
above typically need 40G+ of GPU memory.
````

Source

English

Tools in this stack