LiteLLM으로 여러 LLM을 OpenAI API 하나로 묶기: Fallback, 프록시 게이트웨이, 비용 한도
이 글의 핵심
LiteLLM은 OpenAI Chat Completions 형식 하나로 OpenAI, Anthropic, Gemini, Bedrock, Ollama 등 여러 제공자의 모델을 호출하게 해 주는 오픈소스 라이브러리이자 프록시입니다. SDK와 Proxy 모드의 차이, Fallback과 라우팅, 팀 키와 예산 한도, 캐싱을 설정 예제로 정리하고, 추상화가 새는 지점(제공자별 파라미터 차이, 토큰 계산 오차, 여러 인스턴스에서 한도가 공유되지 않는 문제, 모델 ID 교체)을 함께 짚습니다.
이 글의 핵심
LiteLLM은 OpenAI Chat Completions와 같은 입력·출력 형식으로 OpenAI, Anthropic, Gemini, Mistral, Cohere, Bedrock, Azure OpenAI, Groq, Together, Fireworks, Ollama 등 100개가 넘는 제공자의 모델을 호출할 수 있게 해 주는 오픈소스 라이브러리이자 게이트웨이입니다.
이런 도구가 필요한 이유는 제공자마다 API 모양이 다르기 때문입니다. Anthropic은 system 프롬프트를 메시지 배열 밖의 별도 필드로 받고 max_tokens가 필수이며, Gemini는 contents와 parts 구조를 쓰고, 도구 호출과 스트리밍 이벤트 형식도 모두 다릅니다. 제공자를 하나 추가할 때마다 이 차이를 흡수하는 어댑터 코드를 직접 쓰는 대신, LiteLLM이 요청을 각 제공자 형식으로 번역하고 응답을 다시 OpenAI 형식으로 바꿔 줍니다. 다만 번역에는 한계가 있습니다. 한 제공자에만 있는 기능(Anthropic의 확장 사고, Gemini의 그라운딩 등)은 OpenAI 형식에 대응하는 필드가 없어 제공자별 파라미터로 따로 넘겨야 하고, 같은 temperature 값도 모델마다 효과가 다릅니다. “모델 이름만 바꾸면 된다”는 말은 기본 채팅에서는 대체로 맞지만, 고급 기능으로 갈수록 제공자별 테스트가 필요해집니다.
두 가지 사용 형태가 있습니다.
- Python SDK:
import litellm으로 직접 호출 - Proxy Gateway: 별도 서비스로 띄워 OpenAI API 호환 엔드포인트 제공
여러 팀이 LLM을 공유하는 조직은 Proxy 모드로 팀별 키·사용량 추적·비용 한도·감사 로그를 얻고, 개인이나 단일 서비스는 SDK만으로도 제공자 교체·Fallback·A/B 테스트를 할 수 있습니다.
설치
SDK (파이썬)
pip install litellm # SDK만 쓸 때
pip install "litellm[proxy]" # 프록시 서버를 직접 실행할 때
Proxy (Docker)
docker run -d \
--name litellm \
-p 4000:4000 \
-e LITELLM_MASTER_KEY=sk-master-... \
-e DATABASE_URL=postgresql://... \
-v $(pwd)/config.yaml:/app/config.yaml \
ghcr.io/berriai/litellm-database:main-stable \
--config /app/config.yaml
Helm (K8s)
helm repo add litellm https://berriai.github.io/litellm/
helm install litellm litellm/litellm --values values.yaml
첫 호출: OpenAI SDK가 그대로 동작
from litellm import completion
import os
os.environ["OPENAI_API_KEY"] = "sk-..."
os.environ["ANTHROPIC_API_KEY"] = "sk-ant-..."
os.environ["GEMINI_API_KEY"] = "..."
# GPT-4o
r = completion(
model="gpt-4o",
messages=[{"role": "user", "content": "2026년 핫한 블루오션 SaaS 아이디어 3가지"}],
)
print(r.choices[0].message.content)
# Claude 3.5 Sonnet (단지 model만 바꿈)
r = completion(
model="claude-3-5-sonnet-20241022",
messages=[{"role": "user", "content": "위 아이디어 중 하나를 PRD로 써줘"}],
)
# Gemini
r = completion(
model="gemini/gemini-1.5-pro-latest",
messages=[{"role": "user", "content": "한국 시장 맞춤 피처 10개"}],
)
# Ollama (로컬)
r = completion(
model="ollama/llama3.1",
messages=[{"role": "user", "content": "위 PRD 영어로 번역"}],
api_base="http://localhost:11434",
)
응답 포맷은 모두 OpenAI ChatCompletion 객체로 통일됩니다. 코드 변경 없이 모델만 교체할 수 있는 것이 핵심 가치입니다.
model 문자열의 앞부분(gemini/, ollama/, bedrock/)이 어느 제공자로 보낼지를 정합니다. gpt-4o나 claude-...처럼 잘 알려진 이름은 접두사 없이도 LiteLLM이 알아서 제공자를 추론하지만, 새 모델이 나와 LiteLLM의 모델 목록에 아직 없으면 LLM Provider NOT provided 에러가 나므로 anthropic/모델명처럼 제공자를 명시하는 습관이 안전합니다. 위 예제의 모델 ID들은 작성 시점의 예시이며, 제공자들은 구 모델을 주기적으로 종료하므로 운영 코드에서는 각 제공자의 현재 모델 목록을 확인하고 모델 이름을 설정 파일로 빼 두는 것이 좋습니다. 이 점에서도 뒤의 Proxy 모드가 유리합니다. 애플리케이션은 chat-default 같은 별칭만 쓰고, 실제 모델 교체는 프록시 설정만 바꾸면 되기 때문입니다.
usage 필드의 토큰 수와 LiteLLM이 계산하는 response._hidden_params["response_cost"] 비용은 LiteLLM에 내장된 단가표를 기준으로 합니다. 제공자가 가격을 바꾸거나 할인 계약이 있으면 실제 청구액과 다를 수 있으므로, 예산 한도는 대략적인 가드레일로 보고 정확한 비용은 제공자 콘솔의 청구 데이터와 대조해야 합니다.
Streaming
from litellm import completion
stream = completion(
model="claude-3-5-sonnet-20241022",
messages=[{"role": "user", "content": "쉽게 설명해줘: Merkle Tree"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
print(delta, end="", flush=True)
Anthropic/Gemini의 네이티브 스트리밍 이벤트를 OpenAI SSE 포맷으로 정규화해 내보냅니다. 프론트엔드 SSE 소비 코드를 재사용할 수 있습니다.
스트리밍에서는 토큰 사용량을 따로 챙겨야 합니다. 스트림 응답에는 기본적으로 usage가 담기지 않으므로, 비용을 기록하려면 stream_options={"include_usage": True}를 넘겨 마지막 청크에서 사용량을 받거나, LiteLLM의 stream_chunk_builder로 청크를 합쳐 전체 응답을 복원해야 합니다. 또 사용자가 중간에 연결을 끊어도 제공자 쪽 생성은 계속되어 비용이 발생할 수 있으므로, 서버에서 클라이언트 연결 종료를 감지해 스트림을 닫는 처리가 필요합니다. 첫 청크 이후 네트워크가 끊기면 이미 일부 텍스트를 사용자에게 보낸 상태라 뒤의 Fallback으로 다른 모델에 조용히 넘기기도 어렵다는 점도 기억해야 합니다.
Fallback: 한 provider 장애에도 서비스 유지
from litellm import completion
response = completion(
model="gpt-4o",
messages=[{"role": "user", "content": "..."}],
fallbacks=[
"claude-3-5-sonnet-20241022",
"gemini/gemini-1.5-pro-latest",
],
num_retries=3,
)
OpenAI가 timeout/500을 내면 자동으로 Claude → Gemini 순으로 재시도합니다. SLA 달성을 위한 Multi-provider 전략이 몇 줄로 구현됩니다.
Fallback은 편하지만, 모델이 바뀌면 응답의 성격도 바뀐다는 점을 고려해야 합니다. JSON 형식을 엄격히 지키도록 GPT-4o에 맞춰 다듬은 프롬프트가 다른 모델에서는 형식을 벗어난 응답을 낼 수 있고, 컨텍스트 창 크기가 다른 모델로 넘어가면 긴 입력이 ContextWindowExceededError로 실패합니다(LiteLLM은 이 경우를 위한 context_window_fallbacks를 따로 제공합니다). 사용자가 어떤 모델의 답을 받았는지 알 수 있도록 응답의 model 필드를 로그에 남겨 두면, “가끔 답변 품질이 이상하다”는 문의가 왔을 때 원인을 바로 추적할 수 있습니다. 또 num_retries=3과 Fallback 두 단계를 합치면 최악의 경우 요청 하나가 수십 초 동안 여러 번 시도되므로, 사용자 대기 시간 상한을 정하고 timeout을 함께 맞춰야 합니다.
타임아웃·재시도
completion(
model="gpt-4o",
messages=[...],
timeout=30, # 요청당 30초
num_retries=3, # 재시도 가능한 에러(429, 5xx, 타임아웃)에 한해 재시도
)
재시도는 요청이 실패했을 때만 의미가 있다는 점을 구분해야 합니다. 429(Rate limit)나 503처럼 잠시 뒤 성공할 수 있는 에러는 재시도 대상이지만, 400(잘못된 요청)이나 401(인증 실패)은 몇 번을 다시 보내도 같은 결과이므로 곧바로 실패시키는 편이 낫습니다. 타임아웃은 더 까다롭습니다. 제공자가 이미 생성을 끝냈는데 응답만 늦게 도착한 경우에도 재시도하면 같은 요청의 비용을 두 번 내게 됩니다.
Proxy Gateway: 팀 공유 환경의 필수
config.yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
rpm: 1000
tpm: 60000
- model_name: claude-sonnet
litellm_params:
model: claude-3-5-sonnet-20241022
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: llama-local
litellm_params:
model: ollama/llama3.1
api_base: http://ollama.internal:11434
router_settings:
routing_strategy: latency-based-routing
fallbacks:
- gpt-4o: [claude-sonnet, llama-local]
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
store_model_in_db: true
litellm_settings:
set_verbose: false
cache: true
cache_params:
type: redis
host: redis.internal
port: 6379
success_callback: ["langsmith", "prometheus"]
failure_callback: ["sentry"]
max_budget: 1000 # USD
budget_duration: 30d
실행
litellm --config config.yaml --port 4000
클라이언트는 OpenAI SDK 그대로
from openai import OpenAI
client = OpenAI(
api_key="sk-team-key-...", # LiteLLM이 발급한 팀 키
base_url="http://litellm.internal:4000/v1",
)
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "..."}],
)
클라이언트 코드 관점에서 완전히 투명하게 LiteLLM을 통하게 됩니다.
설정 파일의 model_name은 클라이언트가 부르는 별칭이고, litellm_params.model이 실제로 호출되는 제공자 모델입니다. 같은 model_name으로 여러 항목을 등록하면 LiteLLM은 이를 하나의 모델 그룹으로 보고 라우팅 전략에 따라 나눠 보냅니다(뒤의 가중치 라우팅이 이 방식입니다). api_key: os.environ/OPENAI_API_KEY는 설정 파일에 키를 직접 쓰지 않고 환경 변수에서 읽으라는 LiteLLM 전용 문법입니다. 클라이언트가 등록되지 않은 model 이름을 보내면 Invalid model name passed in model=... 에러가 나므로, 새 모델을 쓰려는 팀이 생기면 프록시 설정에 먼저 추가해야 합니다. 이 과정 자체가 “어떤 팀이 어떤 모델을 쓰는지” 통제하는 지점이 됩니다.
팀 키 발급 + 비용 한도
curl -X POST http://litellm:4000/key/generate \
-H "Authorization: Bearer $MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "team-growth",
"models": ["gpt-4o", "claude-sonnet"],
"max_budget": 100,
"budget_duration": "30d",
"rpm_limit": 60,
"tpm_limit": 10000,
"metadata": {"team": "growth", "env": "prod"}
}'
- 팀/사용자/프로젝트별로 키 발급
- 사용량 누적 달러 집계 (공식 단가표 기반)
- 분당 요청 수·분당 토큰 수 한도 설정
- 메타데이터로 비용 리포트 그룹핑
관리자 UI(/ui)에서 사용량·모델별 분포·지연 시간·오류율을 확인할 수 있습니다.
가상 키와 예산 기능은 데이터베이스가 있어야 동작합니다. DATABASE_URL 없이 프록시를 띄우면 /key/generate 호출이 DB가 연결되지 않았다는 에러로 실패하며, 이 때문에 Docker 이미지도 DB 기능이 포함된 litellm-database를 씁니다. 예산 초과 여부는 요청마다 누적 비용을 확인해 판단하므로, 예산이 거의 다 찬 상태에서 동시에 여러 요청이 들어오면 약간 초과할 수 있다는 점도 알아 둘 만합니다. 마스터 키는 모든 키를 만들고 지울 수 있는 관리자 권한이라 애플리케이션에서 직접 쓰면 안 되고, 서비스마다 필요한 모델과 한도만 허용한 가상 키를 따로 발급해 쓰는 것이 원칙입니다.
지능형 라우팅
지연 시간 기반
router_settings:
routing_strategy: latency-based-routing
여러 배포(Azure OpenAI 여러 리전, OpenAI+Azure 혼용)의 최근 p95 지연 시간을 측정해 가장 빠른 인스턴스로 라우팅.
가용성 기반
router_settings:
routing_strategy: usage-based-routing-v2
rate limit이 남은 배포를 우선 선택. 여러 Azure 리소스를 합친 “큰 한 개 풀”처럼 동작.
가중치 기반 (카나리·A/B)
model_list:
- model_name: chat-model
litellm_params:
model: openai/gpt-4o
model_info:
weight: 90
- model_name: chat-model
litellm_params:
model: claude-3-5-sonnet-20241022
model_info:
weight: 10
“chat-model”을 호출하면 90%는 GPT-4o, 10%는 Claude로 라우팅. 품질 비교 실험에 유용.
가격 기반
router_settings:
routing_strategy: cost-based-routing
가장 저렴한 모델을 선택 (최소 품질 조건은 별도 fallback 체인으로 관리).
라우팅 전략은 같은 모델 그룹 안에서 어느 배포로 보낼지를 정하는 것이지, 요청 내용을 보고 쉬운 질문은 싼 모델로, 어려운 질문은 비싼 모델로 보내 주는 기능이 아닙니다. 그런 판단이 필요하면 애플리케이션이 작업 종류에 따라 다른 model_name을 부르도록 설계해야 합니다. 또 지연 시간과 사용량 기반 라우팅은 각 배포의 최근 상태를 추적해야 하는데, 프록시를 여러 인스턴스로 띄우면 이 상태가 인스턴스마다 따로 관리됩니다. rpm, tpm 한도도 마찬가지라 인스턴스가 3개면 실제 허용량이 3배가 되므로, router_settings에 redis_host, redis_port를 지정해 인스턴스들이 Redis로 상태를 공유하게 해야 합니다.
캐싱
litellm_settings:
cache: true
cache_params:
type: redis # 정확히 같은 요청만 재사용
host: redis
port: 6379
ttl: 600
# 의미 기반 캐시는 별도 타입과 임베딩 모델이 필요
# type: redis-semantic
# similarity_threshold: 0.95
# redis_semantic_cache_embedding_model: text-embedding-3-small
- Exact caching: 동일 프롬프트·파라미터의 응답 재사용
- Semantic caching: 요청 임베딩의 유사도가 임계값 이상이면 캐시 히트
- 지원 백엔드: Redis, S3, 로컬 메모리, Qdrant(의미 기반), DynamoDB 등
캐시가 얼마나 도움이 되는지는 요청이 얼마나 반복되는지에 달려 있습니다. FAQ 챗봇처럼 같은 질문이 반복되는 서비스에서는 효과가 크지만, 대화 이력이 매번 달라지는 채팅에서는 전체 메시지 배열이 캐시 키가 되므로 거의 적중하지 않습니다. 의미 기반 캐시는 적중률을 올려 주는 대신 위험이 있습니다. “서울 날씨”와 “부산 날씨”처럼 문장 구조가 거의 같은 질문의 임베딩이 임계값을 넘으면 다른 질문에 대한 답이 돌아갑니다. 또 요청마다 임베딩 API를 호출하는 비용과 지연이 추가됩니다. 사용자별 정보가 담긴 응답을 캐시하면 다른 사용자에게 그 내용이 노출될 수 있으므로, 개인화된 요청은 캐시에서 제외하거나 캐시 키에 사용자 구분을 넣어야 합니다.
관측: 로그·메트릭·트레이스
litellm_settings:
success_callback:
- langsmith
- langfuse
- prometheus
- datadog
- helicone
failure_callback:
- sentry
- slack
prometheus 콜백을 켜면 프록시의 /metrics 엔드포인트로 요청 수, 지연 시간, 토큰 사용량, 비용 같은 지표가 모델·키·팀 레이블과 함께 노출됩니다. 정확한 지표 이름은 LiteLLM 버전마다 바뀌어 왔으므로, 대시보드를 만들기 전에 실제 /metrics 출력을 확인하는 것이 안전합니다. 일부 연동 기능은 엔터프라이즈 라이선스가 필요한 경우가 있어 사용 전에 문서를 확인해야 합니다.
콜백으로 외부 서비스에 로그를 보내면 프롬프트와 응답 전문이 그 서비스로 전송된다는 점을 잊기 쉽습니다. 사용자의 개인정보나 사내 문서가 담긴 요청이라면 데이터 처리 계약이 없는 외부 관측 서비스로 내보내는 것 자체가 문제가 될 수 있으므로, 메시지 본문 로깅을 끄는 옵션(turn_off_message_logging)이나 자체 호스팅한 Langfuse 같은 대안을 검토해야 합니다.
비용 최적화 전략
모델 계층화
간단 분류/라우팅 → gpt-4o-mini, claude-haiku, gemini-1.5-flash
복잡 reasoning → gpt-4o, claude-sonnet, gemini-1.5-pro
매우 어려운 추론 → o1, claude-opus (fallback 전용)
프롬프트 캐시 활용
OpenAI와 Anthropic은 반복되는 긴 프롬프트 앞부분을 캐시해 입력 토큰 단가를 낮추는 기능을 제공합니다. OpenAI는 일정 길이 이상의 같은 접두사를 자동으로 캐시하고, Anthropic은 캐시할 위치를 메시지 블록에 cache_control로 직접 표시해야 합니다. LiteLLM은 이 표시를 OpenAI 형식 메시지 안에 그대로 적을 수 있게 해 줍니다.
completion(
model="anthropic/claude-sonnet-model-id", # 사용 중인 Claude 모델 ID로 교체
messages=[
{
"role": "system",
"content": [
{
"type": "text",
"text": LONG_SYSTEM_PROMPT, # 매 요청에 반복되는 긴 지침·문서
"cache_control": {"type": "ephemeral"}, # 여기까지를 캐시
}
],
},
{"role": "user", "content": "질문"},
],
)
캐시는 접두사가 정확히 같을 때만 적중합니다. 시스템 프롬프트 맨 앞에 현재 시각이나 사용자 이름을 넣으면 요청마다 접두사가 달라져 캐시가 한 번도 쓰이지 않으므로, 변하지 않는 내용을 앞에, 바뀌는 내용을 뒤에 두어야 합니다. 캐시가 실제로 쓰였는지는 응답 usage의 캐시 관련 토큰 수로 확인할 수 있습니다. 예전 자료에 보이는 anthropic-beta: prompt-caching-... 헤더는 이 기능이 베타였을 때의 방식이라 지금은 필요하지 않습니다.
배치 처리: 병렬 호출과 배치 API는 다르다
LiteLLM의 batch_completion은 여러 요청을 동시에 보내 주는 편의 함수일 뿐, 가격 할인이 있는 제공자의 Batch API와는 다릅니다.
from litellm import batch_completion
# 여러 대화를 병렬로 호출 (일반 단가, 즉시 응답)
responses = batch_completion(
model="gpt-4o-mini",
messages=[
[{"role": "user", "content": "문장 1 요약"}],
[{"role": "user", "content": "문장 2 요약"}],
],
)
OpenAI와 Anthropic의 Batch API는 요청을 파일로 올려 두면 최대 24시간 안에 처리해 주는 비동기 방식으로, 실시간성이 필요 없는 대량 작업을 일반 요청보다 낮은 단가로 처리할 수 있습니다. LiteLLM에서는 create_file로 JSONL 입력을 올리고 create_batch로 작업을 만든 뒤 상태를 조회하는 파일·배치 API로 다룹니다. 두 방식을 혼동해 수천 건을 batch_completion으로 한꺼번에 보내면 할인은커녕 곧바로 Rate limit에 걸리므로, 병렬 호출에는 동시 실행 수 제한을 함께 두는 것이 좋습니다.
tokenizer 기반 예산 체크
from litellm import token_counter
tokens = token_counter(model="gpt-4o", messages=messages)
if tokens > 10000:
raise ValueError("프롬프트가 너무 깁니다. 청크 분할 필요.")
구조화 출력 (Structured Outputs)
from pydantic import BaseModel
from litellm import completion
class Person(BaseModel):
name: str
age: int
skills: list[str]
r = completion(
model="gpt-4o",
messages=[{"role": "user", "content": "예시 인물 데이터 생성"}],
response_format=Person,
)
person = Person.model_validate_json(r.choices[0].message.content)
OpenAI/Anthropic/Gemini 전반에서 JSON Schema 기반 구조화 응답을 얻을 수 있게 정규화합니다.
정규화의 방식은 제공자마다 다릅니다. 스키마를 네이티브로 강제하는 제공자에는 그대로 전달하지만, 그렇지 않은 모델에서는 도구 호출이나 프롬프트 지시로 흉내 내므로 스키마를 벗어난 응답이 올 가능성이 남아 있습니다. litellm.supports_response_schema(model=...)로 해당 모델이 스키마 강제를 지원하는지 확인할 수 있고, 어느 경우든 위처럼 Pydantic의 model_validate_json으로 한 번 더 검증해 ValidationError를 처리하는 코드를 두는 것이 안전합니다. 응답이 max_tokens에 걸려 잘리면 JSON이 중간에 끊겨 파싱이 실패하므로, finish_reason이 length인지도 함께 확인해야 합니다.
함수 호출 (Tool Use)
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "날씨 조회",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
r = completion(
model="claude-3-5-sonnet-20241022",
messages=[{"role": "user", "content": "서울 날씨 알려줘"}],
tools=tools,
)
tool_call = r.choices[0].message.tool_calls[0]
Anthropic의 tool_use·Gemini의 function_call·OpenAI tool_calls를 모두 OpenAI tool_calls 포맷으로 통일합니다.
tool_calls를 받은 뒤에는 도구를 실행하고 그 결과를 {"role": "tool", "tool_call_id": tool_call.id, "content": ...} 메시지로 대화에 덧붙여 다시 completion을 호출해야 최종 답변이 나옵니다. 모델이 도구를 부르지 않기로 판단하면 tool_calls가 None이므로 [0]으로 바로 접근하는 위 코드는 TypeError가 날 수 있습니다. tool_call.function.arguments는 JSON 문자열이라 json.loads로 파싱해야 하고, 모델이 스키마에 맞지 않는 인자를 만들 수도 있으니 실행 전에 검증해야 합니다. 대화 중간에 제공자를 바꾸는 Fallback이 일어나면 이전 모델이 만든 도구 호출 ID와 형식을 다음 모델이 그대로 받아들이지 않는 경우가 있어, 도구 호출이 포함된 대화는 한 모델로 끝까지 진행하는 편이 안정적입니다.
프로덕션 배포 패턴
Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: litellm
spec:
replicas: 3
template:
spec:
containers:
- name: litellm
image: ghcr.io/berriai/litellm-database:main-stable
args: ["--config", "/config/config.yaml"]
envFrom:
- secretRef: {name: litellm-secrets}
resources:
requests: {memory: 512Mi, cpu: 200m}
limits: {memory: 2Gi, cpu: 2}
livenessProbe:
httpGet: {path: /health/liveliness, port: 4000}
readinessProbe:
httpGet: {path: /health/readiness, port: 4000}
volumeMounts:
- {name: config, mountPath: /config}
volumes:
- name: config
configMap: {name: litellm-config}
3 replicas 이상 + HPA로 자동 확장, Redis 캐시 공유, Postgres로 키·사용량 영속.
여러 레플리카를 띄울 때는 앞의 라우팅 절에서 말한 것처럼 Redis를 router_settings에도 연결해야 rate limit과 라우팅 상태가 공유됩니다. LLM 응답은 수십 초씩 걸리는 경우가 많아, 앞단 로드 밸런서나 Ingress의 기본 유휴 타임아웃(대개 60초)에 긴 요청이 끊기는 일도 흔합니다. 스트리밍이 아닌 긴 생성 요청이 504로 실패한다면 LiteLLM보다 앞단의 타임아웃을 먼저 확인해야 합니다. main-stable 같은 움직이는 태그는 재시작할 때마다 다른 버전이 뜰 수 있으므로, 운영에서는 특정 버전 태그로 고정하고 업그레이드는 스테이징에서 먼저 검증하는 편이 안전합니다.
Docker Compose
services:
litellm:
image: ghcr.io/berriai/litellm-database:main-stable
ports: ["4000:4000"]
env_file: .env
volumes: ["./config.yaml:/app/config.yaml"]
command: ["--config", "/app/config.yaml"]
depends_on: [postgres, redis]
postgres:
image: postgres:16
environment:
POSTGRES_USER: litellm
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: ["pg:/var/lib/postgresql/data"]
redis:
image: redis:7
volumes:
pg:
보안 체크리스트
-
LITELLM_MASTER_KEY는 환경변수·Vault에만 저장 - 팀 키는 최소 권한 (허용 모델·금액 한도·만료)
- HTTPS 앞단 (nginx/Cloudflare) 필수
- DB/Redis는 내부망·TLS
- PII 마스킹 콜백(
success_callback이전 단계) 설정 - 프롬프트 주입 방지: 시스템 프롬프트 누출 대비 content filter
- 로깅 시 API 키 redaction (기본 적용)
트러블슈팅
Provider별 예외 처리
import litellm
from litellm import completion
try:
r = completion(model="gpt-4o", messages=[...])
except litellm.exceptions.AuthenticationError as e:
... # 401: 키 확인, 재시도 무의미
except litellm.exceptions.RateLimitError as e:
... # 429: 백오프 후 재시도 또는 다른 배포로
except litellm.exceptions.ContentPolicyViolationError as e:
... # 안전 필터 거부: 사용자에게 안내
OpenAI/Anthropic/Gemini의 서로 다른 에러 코드를 통일된 예외 계층으로 매핑합니다.
Token mismatch
제공자마다 토크나이저가 달라 같은 문장도 토큰 수가 다르게 계산됩니다. LiteLLM의 token_counter는 OpenAI 모델에는 tiktoken을 쓰지만, 공개 토크나이저가 없는 모델에는 근사치를 쓰므로 실제 과금 토큰 수와 어긋날 수 있습니다. 컨텍스트 한도 직전까지 꽉 채우는 로직이라면 여유분을 두고, 정확한 값은 응답의 usage로 확인하는 것이 안전합니다. 특히 한국어는 영어보다 같은 글자 수에 토큰이 많이 드는 경우가 많아, 영어 기준으로 잡은 예산이 쉽게 초과됩니다.
Gemini 응답 필드 누락
Gemini는 content filter로 내용이 비어 있을 수 있음. r.choices[0].finish_reason == "content_filter"를 체크.
함수 호출 포맷 불일치
같은 tools 스펙이라도 Claude·Gemini·OpenAI 각각 미묘한 파라미터 포맷 차이가 있습니다. response_format과 tools는 LiteLLM이 정규화하지만, 각 provider의 test를 CI에 포함해 회귀를 잡으세요.
마무리
AI 기능을 처음 만들 때는 단일 제공자의 공식 SDK로 시작하는 것이 가장 단순합니다. 제공자를 둘 이상 써야 할 이유(장애 대비, 비용이 다른 모델 조합, 데이터 거주 요건, 팀별 예산 관리)가 실제로 생겼을 때 LiteLLM을 도입하면, 제공자별 어댑터와 키 관리를 직접 만드는 수고를 크게 덜 수 있습니다. 대신 추상화 계층이 하나 늘어나므로, 제공자별 고유 기능은 따로 넘겨야 하고, 토큰과 비용 계산은 근사치이며, 프록시를 쓴다면 그 자체가 운영해야 할 핵심 인프라가 된다는 점을 받아들여야 합니다. 도입 초기에 쓰는 모델마다 대표 요청으로 자동 테스트를 만들어 두면, 모델이나 LiteLLM 버전을 바꿀 때 동작 차이를 일찍 발견할 수 있습니다.
자주 묻는 질문 (FAQ)
Q. provider마다 다른 에러 코드는 LiteLLM에서 어떻게 처리하나요?
A. LiteLLM은 OpenAI·Anthropic·Gemini의 서로 다른 에러를 AuthenticationError(401), RateLimitError(429), ContentPolicyViolationError 같은 통일된 예외 계층으로 매핑합니다. 따라서 provider별 분기 없이 예외 타입만 잡아 재시도나 fallback을 결정하면 됩니다. 다만 Gemini는 content filter로 응답이 비어 올 수 있으므로 finish_reason == "content_filter"를 따로 확인하는 편이 안전합니다.
같이 보면 좋은 글
- ChatGPT API 실전 가이드 | OpenAI API로 AI 애플리케이션 만들기
- Claude API 사용법
- Vercel AI SDK로 AI 챗 만들기: 최소 구성부터 스트리밍·도구 호출·RAG·멀티 프로바이더까지