본문 바로가기

AI 개발

LiteLLM 완전 가이드 3편 — Proxy 서버 모드: 팀 공용 LLM 게이트웨이 구축 실전

반응형

1·2편은 Python 라이브러리로 개인이 직접 쓰는 방법이었습니다. 3편은 팀 전체가 쓰는 방법입니다. LiteLLM Proxy를 띄우면 팀원들은 각자 API 키 없이 http://our-gateway.com:4000으로 요청하면 됩니다. 비용은 중앙에서 집계되고, 팀별 예산도 설정됩니다.

[3편 핵심 요약]
→ LiteLLM Proxy: 팀 공용 OpenAI 호환 LLM 게이트웨이 — 셀프호스팅
→ 구성: config.yaml(모델·설정) + .env(API 키) + PostgreSQL(비용 추적) + Redis(고트래픽)
→ 포트: 4000 (API) / UI: http://localhost:4000/ui
→ Master Key: 관리자 키 (sk-로 시작) — 가상 키 발급에 사용
→ Virtual Key: 팀원·프로젝트별 발급 — 모델 접근·예산·RPM 제한 설정 가능
→ 주의: v1.82.7, v1.82.8 — 2026년 3월 공급망 보안 사고 — 즉시 v1.83.0+로 업그레이드
→ 프로덕션 최소 사양: 4 CPU 코어, 8GB RAM
→ Claude Code·Cursor도 proxy base_url로 연결 가능 — 팀 전체 비용 통합

실전 1 — 빠른 시작 (CLI)

# 설치
pip install 'litellm[proxy]'
# 또는
uv tool install 'litellm[proxy]'

# 단일 모델로 즉시 시작 (테스트용)
export ANTHROPIC_API_KEY="sk-ant-..."
litellm --model anthropic/claude-sonnet-4-6

# → http://0.0.0.0:4000 에서 실행
# → OpenAI 호환 API 즉시 사용 가능
# 클라이언트에서 proxy 사용
from openai import OpenAI

client = OpenAI(
    api_key="anything",             # proxy는 자체 인증 사용
    base_url="http://localhost:4000"
)

response = client.chat.completions.create(
    model="claude-sonnet-4-6",  # config.yaml의 model_name
    messages=[{"role": "user", "content": "안녕"}]
)
print(response.choices[0].message.content)

실전 2 — config.yaml 완전 가이드

# config.yaml — LiteLLM Proxy 핵심 설정 파일

# ── 모델 목록 ──────────────────────────────────────────
model_list:

  # Claude (Anthropic 직접)
  - model_name: claude-sonnet       # 클라이언트에서 쓸 이름
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_key: os.environ/ANTHROPIC_API_KEY  # env에서 읽음
    model_info:
      id: claude-sonnet-v1  # 고유 ID

  # Claude (AWS Bedrock 경유 — 동일 그룹으로 로드밸런싱)
  - model_name: claude-sonnet       # 같은 이름 → 자동 분산
    litellm_params:
      model: bedrock/anthropic.claude-sonnet-4-6
      aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID
      aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY
      aws_region_name: us-east-1
    model_info:
      id: claude-sonnet-bedrock-v1

  # GPT
  - model_name: gpt
    litellm_params:
      model: openai/gpt-5.4
      api_key: os.environ/OPENAI_API_KEY

  # Gemini Flash (저렴한 기본 모델)
  - model_name: gemini-flash
    litellm_params:
      model: gemini/gemini-3-flash
      api_key: os.environ/GEMINI_API_KEY

  # Ollama 로컬 모델 (비용 없음)
  - model_name: llama
    litellm_params:
      model: ollama/llama3.3
      api_base: http://ollama:11434  # Docker 네트워크

  # 폴백 체인 설정
  - model_name: default
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_key: os.environ/ANTHROPIC_API_KEY

# ── 라우터 설정 ────────────────────────────────────────
router_settings:
  routing_strategy: latency-based-routing  # 가장 빠른 프로바이더
  num_retries: 3
  timeout: 30
  fallbacks:
    - claude-sonnet: ["gpt", "gemini-flash"]
    - gpt: ["claude-sonnet", "gemini-flash"]
  context_window_fallbacks:
    - claude-sonnet: ["gemini-flash"]  # 200K → 1M 컨텍스트

# ── 일반 설정 ──────────────────────────────────────────
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY  # 관리자 키
  database_url: os.environ/DATABASE_URL       # PostgreSQL

  # UI 로그인
  ui_username: os.environ/UI_USERNAME
  ui_password: os.environ/UI_PASSWORD

  # CORS (웹 클라이언트 허용)
  allow_requests_on_db_unavailable: true  # DB 다운 시도 요청 허용

# ── LiteLLM 설정 ───────────────────────────────────────
litellm_settings:
  # 캐시 (Redis)
  cache: true
  cache_params:
    type: redis
    host: os.environ/REDIS_HOST
    port: 6379
    password: os.environ/REDIS_PASSWORD
    ttl: 3600

  # 성공 콜백 (로깅)
  success_callback: ["langfuse"]

  # 비용 추적
  callbacks: ["langfuse"]

실전 3 — Docker Compose 프로덕션 배포

# docker-compose.yml
version: "3.9"

services:
  # LiteLLM Proxy
  litellm:
    image: ghcr.io/berriai/litellm:v1.83.2-stable  # ⚠️ 버전 고정 필수
    # v1.82.7, v1.82.8 사용 금지 (2026년 3월 공급망 보안 사고)
    ports:
      - "4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml:ro  # 읽기 전용 마운트
    env_file:
      - .env
    command: ["--config", "/app/config.yaml", "--port", "4000"]
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    # 프로덕션 최소 사양
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: 8G

  # PostgreSQL — 비용 추적·가상 키 저장
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: litellm
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # Redis — 고트래픽 캐싱·레이트 리밋 (1000+ RPS 시 필수)
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
# .env — API 키 및 설정
LITELLM_MASTER_KEY=sk-my-admin-key-2026  # 반드시 sk- 시작
DATABASE_URL=postgresql://litellm:${DB_PASSWORD}@db:5432/litellm
DB_PASSWORD=your-secure-db-password
REDIS_HOST=redis
REDIS_PASSWORD=your-redis-password

# LLM 프로바이더 키
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=...
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

# Admin UI 로그인
UI_USERNAME=admin
UI_PASSWORD=your-ui-password

# Langfuse 연동 (선택)
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
# 실행
docker compose up -d

# 헬스체크
curl http://localhost:4000/health
# → {"status": "healthy", "litellm_version": "1.83.2", ...}

# 로그 확인
docker compose logs -f litellm

# Admin UI 접속
# http://localhost:4000/ui

실전 4 — 가상 키(Virtual Key) 관리

팀원에게 실제 API 키 대신 가상 키를 발급합니다. 각 키에 모델 접근·예산·RPM 제한을 설정합니다.

# ── 키 발급 ───────────────────────────────────────────
# 프론트엔드 팀용 키 (Claude만 접근, 월 $50 예산)
curl -X POST 'http://localhost:4000/key/generate' \
  -H 'Authorization: Bearer sk-my-admin-key-2026' \
  -H 'Content-Type: application/json' \
  -d '{
    "key_alias": "frontend-team",
    "models": ["claude-sonnet", "gemini-flash"],
    "max_budget": 50,
    "budget_duration": "monthly",
    "rpm_limit": 100,
    "tpm_limit": 500000,
    "metadata": {
      "team": "frontend",
      "created_by": "admin"
    }
  }'

# 응답: {"key": "sk-frontend-xxxx", "expires": null, ...}
# ── AI 코딩 툴 전용 키 (Opus 포함, 높은 예산) ─────────
curl -X POST 'http://localhost:4000/key/generate' \
  -H 'Authorization: Bearer sk-my-admin-key-2026' \
  -H 'Content-Type: application/json' \
  -d '{
    "key_alias": "claude-code-team",
    "models": ["claude-sonnet", "claude-opus-4-7", "gpt"],
    "max_budget": 200,
    "budget_duration": "monthly",
    "rpm_limit": 500,
    "metadata": {"team": "engineering", "use_case": "coding"}
  }'
# Python으로 키 관리
import httpx

PROXY_URL = "http://localhost:4000"
MASTER_KEY = "sk-my-admin-key-2026"

def create_virtual_key(
    alias: str,
    models: list[str],
    monthly_budget: float,
    rpm_limit: int = 100,
    team_id: str = None,
) -> str:
    """가상 키 발급"""
    payload = {
        "key_alias": alias,
        "models": models,
        "max_budget": monthly_budget,
        "budget_duration": "monthly",
        "rpm_limit": rpm_limit,
    }
    if team_id:
        payload["team_id"] = team_id

    response = httpx.post(
        f"{PROXY_URL}/key/generate",
        headers={"Authorization": f"Bearer {MASTER_KEY}"},
        json=payload
    )
    return response.json()["key"]

def list_keys() -> list[dict]:
    """전체 가상 키 목록"""
    response = httpx.get(
        f"{PROXY_URL}/key/list",
        headers={"Authorization": f"Bearer {MASTER_KEY}"}
    )
    return response.json().get("keys", [])

def get_key_spend(key: str) -> dict:
    """특정 키 사용량 조회"""
    response = httpx.get(
        f"{PROXY_URL}/key/info",
        headers={"Authorization": f"Bearer {MASTER_KEY}"},
        params={"key": key}
    )
    return response.json()

def delete_key(key: str) -> bool:
    """가상 키 삭제"""
    response = httpx.delete(
        f"{PROXY_URL}/key/delete",
        headers={"Authorization": f"Bearer {MASTER_KEY}"},
        json={"keys": [key]}
    )
    return response.status_code == 200

# 팀별 키 자동 발급 예시
teams = [
    {"name": "backend-team",  "models": ["claude-sonnet", "gpt"], "budget": 100},
    {"name": "frontend-team", "models": ["claude-sonnet", "gemini-flash"], "budget": 50},
    {"name": "data-team",     "models": ["gpt", "gemini-flash"], "budget": 30},
    {"name": "devops-team",   "models": ["llama", "gemini-flash"], "budget": 10},
]

for team in teams:
    key = create_virtual_key(
        alias=team["name"],
        models=team["models"],
        monthly_budget=team["budget"],
    )
    print(f"✅ {team['name']}: {key}")

실전 5 — 클라이언트 연동 (OpenAI SDK, Anthropic SDK, Claude Code)

# ── OpenAI SDK ───────────────────────────────────────
from openai import OpenAI

client = OpenAI(
    api_key="sk-frontend-xxxx",      # 팀원 가상 키
    base_url="http://our-proxy.company.com:4000"
)

response = client.chat.completions.create(
    model="claude-sonnet",  # config.yaml의 model_name
    messages=[{"role": "user", "content": "안녕"}]
)
# ── Anthropic SDK (Proxy 경유) ─────────────────────────
from anthropic import Anthropic

client = Anthropic(
    api_key="sk-engineering-xxxx",   # 가상 키
    base_url="http://our-proxy.company.com:4000"
)

message = client.messages.create(
    model="claude-sonnet-4-6",       # 원본 모델명 또는 별칭
    max_tokens=1024,
    messages=[{"role": "user", "content": "안녕"}]
)
# ── Claude Code에서 Proxy 연결 ─────────────────────────
# Claude Code → 팀 proxy 경유 → 비용 통합 추적

# 환경변수 설정
export ANTHROPIC_BASE_URL="http://our-proxy.company.com:4000"
export ANTHROPIC_API_KEY="sk-claude-code-team-xxxx"  # 가상 키

# 이후 Claude Code 실행 시 proxy 경유
claude

# 또는 CLAUDE.md에 설정 가능
# ── Cursor에서 Proxy 연결 ─────────────────────────────
# Cursor Settings → Models → OpenAI API Key
# Base URL: http://our-proxy.company.com:4000
# API Key: sk-frontend-team-xxxx (가상 키)
# Model: claude-sonnet (config.yaml의 model_name)

실전 6 — 팀 관리 및 비용 모니터링

# ── 팀 생성 ───────────────────────────────────────────
curl -X POST 'http://localhost:4000/team/new' \
  -H 'Authorization: Bearer sk-my-admin-key-2026' \
  -H 'Content-Type: application/json' \
  -d '{
    "team_alias": "engineering",
    "max_budget": 300,
    "budget_duration": "monthly",
    "models": ["claude-sonnet", "gpt", "claude-opus-4-7"],
    "rpm_limit": 1000
  }'

# ── 팀 멤버 추가 ──────────────────────────────────────
curl -X POST 'http://localhost:4000/team/member_add' \
  -H 'Authorization: Bearer sk-my-admin-key-2026' \
  -H 'Content-Type: application/json' \
  -d '{
    "team_id": "team-engineering-xxxx",
    "member": [
      {"role": "user", "user_id": "alice@company.com"},
      {"role": "admin", "user_id": "bob@company.com"}
    ]
  }'
# 팀별 비용 리포트 자동화
import httpx
from datetime import datetime, timedelta

def get_team_spend_report() -> list[dict]:
    """팀별 이번 달 비용 리포트"""
    response = httpx.get(
        "http://localhost:4000/spend/teams",
        headers={"Authorization": f"Bearer {MASTER_KEY}"},
    )
    teams = response.json().get("teams", [])

    report = []
    for team in teams:
        report.append({
            "team": team.get("team_alias", "unknown"),
            "spend": team.get("spend", 0),
            "budget": team.get("max_budget", 0),
            "usage_pct": team.get("spend", 0) / max(team.get("max_budget", 1), 1) * 100
        })

    return sorted(report, key=lambda x: x["spend"], reverse=True)

def get_model_spend_report() -> list[dict]:
    """모델별 비용 리포트"""
    response = httpx.get(
        "http://localhost:4000/spend/models",
        headers={"Authorization": f"Bearer {MASTER_KEY}"},
    )
    return response.json().get("models", [])

# 실행 및 출력
teams = get_team_spend_report()
print(f"\n[팀별 비용 리포트 — {datetime.now().strftime('%Y-%m')}]")
for t in teams:
    bar = "█" * int(t["usage_pct"] / 5)
    print(f"  {t['team'][:20]:<20} ${t['spend']:.2f}/${t['budget']:.0f} [{bar:<20}] {t['usage_pct']:.1f}%")

실전 7 — Langfuse 로깅 연동

# config.yaml에 추가
litellm_settings:
  success_callback: ["langfuse"]
  failure_callback: ["langfuse"]
# .env에 추가
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://cloud.langfuse.com  # 또는 셀프호스팅 URL
# 클라이언트에서 메타데이터 추가 (Langfuse 추적용)
from openai import OpenAI

client = OpenAI(
    api_key="sk-team-xxxx",
    base_url="http://localhost:4000"
)

response = client.chat.completions.create(
    model="claude-sonnet",
    messages=[{"role": "user", "content": "코드 리뷰해줘"}],
    extra_body={
        "metadata": {
            # Langfuse 추적 메타데이터
            "trace_name": "code-review",
            "trace_user_id": "user-alice",
            "trace_session_id": "session-123",
            "tags": ["code-review", "claude", "production"],
            "generation_name": "code_review_v2",
        }
    }
)
[Langfuse 대시보드에서 확인 가능한 것들]
→ 요청별 프롬프트·응답 전체 로그
→ 모델별·사용자별·세션별 비용
→ 레이턴시 P50/P95/P99
→ 에러율 트래킹
→ 프롬프트 버전 관리
→ 평가(Evaluation) 점수 추적

마무리

✅ 3편에서 한 것
→ config.yaml 전체 구조 (모델·라우터·일반 설정)
→ Docker Compose 프로덕션 배포 (Proxy + PostgreSQL + Redis)
→ 보안 주의: v1.82.7/1.82.8 사용 금지, 버전 고정 필수
→ 가상 키 발급·관리 (API + Python)
→ OpenAI SDK / Anthropic SDK / Claude Code / Cursor 연동
→ 팀 생성·멤버 관리·예산 설정
→ 팀별·모델별 비용 리포트 자동화
→ Langfuse 로깅 연동

❌ 4편에서 다룰 것
→ LangChain·LangGraph에서 Proxy 통합
→ 가드레일(콘텐츠 필터, PII 마스킹)
→ Prometheus + Grafana 메트릭
→ Kubernetes/Helm 배포
→ 고가용성 설정 (multi-instance + Redis)
→ 프로덕션 보안 (HTTPS, 방화벽)

관련 글

 

반응형