- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 개요
- 1. 설치와 첫 호출
- 2. 여러 공급자를 같은 코드로 호출
- 3. LiteLLM으로 OpenRouter 쓰기
- 4. 스트리밍과 비동기
- 5. Router — 로드 밸런싱과 폴백
- 6. Proxy Server (AI Gateway)
- 7. SDK vs Proxy, 그리고 실전 팁
- 참고 자료
개요
LLM 애플리케이션을 만들다 보면 곧 같은 문제에 부딪힌다. 공급자마다 SDK가 다르고, 요청 파라미터가 다르고, 응답 형태가 다르다. OpenAI 코드로 시작했다가 Anthropic으로 바꾸려면 클라이언트 초기화부터 메시지 파싱까지 전부 손봐야 한다. LiteLLM은 바로 이 지점을 없앤다.
LiteLLM(BerriAI 제작)은 두 가지로 구성된다. 하나는 Python SDK이고, 다른 하나는 Proxy Server(AI Gateway)다. 둘 다 공통 목표가 있다. 100개 이상의 LLM API를 OpenAI 요청/응답 포맷으로 통일해서 호출한다는 것이다. 여기에 비용 추적, 재시도, 로드 밸런싱, 폴백, 가드레일, 로깅 같은 운영 기능이 얹힌다.
이 글은 빠르게 생산성을 얻기 위한 실전 퀵스타트다. 설치하고 첫 호출을 날리고, 여러 공급자를 바꿔 끼우고, 스트리밍과 Router, Proxy까지 최소 경로로 훑는다. LiteLLM의 내부 구조와 심화 운영 패턴을 깊게 다룬 LiteLLM 완전 가이드가 이미 이 블로그에 있으니, 원리와 프로덕션 세부 설정이 필요하면 그쪽을 함께 보면 된다. 이 글은 의도적으로 가볍게 유지한다.
핵심 관점 하나를 먼저 잡고 가자. LiteLLM은 "또 하나의 공급자"가 아니라, 여러 공급자 위에 놓는 단일 통합 계층이다. 그리고 그 아래에는 OpenRouter처럼 다시 수백 개 모델을 묶어주는 라우터도 넣을 수 있다. 즉 LiteLLM 하나로 직접 연결한 공급자와 OpenRouter를 통한 공급자를 모두 같은 코드로 부를 수 있다.
1. 설치와 첫 호출
설치는 한 줄이다. pip을 쓰든 uv를 쓰든 차이가 없다.
pip install litellm
# uv 사용 시
uv add litellm
가장 작은 호출은 다음과 같다. completion 함수 하나에 model과 messages만 넘기면 된다. 응답은 OpenAI 형태 그대로 온다.
from litellm import completion
import os
os.environ["OPENAI_API_KEY"] = "sk-..."
resp = completion(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
여기서 눈여겨볼 점은 응답 접근 방식이다. resp.choices[0].message.content는 OpenAI SDK를 써봤다면 익숙한 그 구조다. 어떤 공급자를 부르든 이 형태가 유지되기 때문에, 응답을 다루는 코드는 한 번만 짜면 된다.
SDK 단계에서도 간단한 안정성 옵션을 바로 줄 수 있다. num_retries로 재시도 횟수를, timeout으로 응답 대기 시간을 지정하면 일시적 오류에 조금 더 강해진다. 이런 옵션은 Router 없이 단일 호출에서도 동작한다.
from litellm import completion
resp = completion(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
num_retries=2,
timeout=30,
)
print(resp.choices[0].message.content)
2. 여러 공급자를 같은 코드로 호출
공급자를 바꾸는 방법은 두 단계뿐이다. model 문자열의 접두사(prefix) 를 바꾸고, 그 공급자의 환경 변수를 설정한다. 그게 전부다. 메시지 구조도, 응답 파싱도 그대로다.
| 공급자 | model 접두사 예시 | 환경 변수 |
|---|---|---|
| OpenAI | openai/gpt-4o | OPENAI_API_KEY |
| Anthropic | anthropic/claude-3-5-sonnet-20241022 | ANTHROPIC_API_KEY |
| Gemini (AI Studio) | gemini/gemini-1.5-pro | GEMINI_API_KEY |
| Vertex AI | vertex_ai/gemini-1.5-pro | GCP 자격 증명 |
| AWS Bedrock | bedrock/... | AWS 자격 증명 |
| Azure OpenAI | azure/... | Azure 설정 |
| Ollama (로컬) | ollama/llama3 | 없음 (로컬 실행) |
예를 들어 Anthropic으로 바꾸면 이렇게 된다.
from litellm import completion
import os
os.environ["ANTHROPIC_API_KEY"] = "sk-ant-..."
resp = completion(
model="anthropic/claude-3-5-sonnet-20241022",
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
로컬 모델로 실험하고 싶다면 Ollama를 띄운 뒤 접두사만 ollama/로 바꾸면 된다. API 키도 필요 없다.
from litellm import completion
resp = completion(
model="ollama/llama3",
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
이 구조 덕분에 "개발은 로컬 Ollama, 스테이징은 Gemini, 프로덕션은 Bedrock" 같은 전환을 코드 변경 없이 환경 변수와 모델 문자열만으로 처리할 수 있다.
3. LiteLLM으로 OpenRouter 쓰기
여기가 이 글에서 특히 강조하고 싶은 조합이다. OpenRouter는 그 자체로 300개 이상의 모델을 하나의 API로 묶어주는 서비스다. LiteLLM을 그 위에 얹으면, OpenRouter의 넓은 모델 커버리지에 LiteLLM의 균일한 인터페이스와 운영 기능(재시도, 폴백, 비용 추적)을 함께 얻는다.
방법은 앞과 동일한 패턴이다. OPENROUTER_API_KEY를 설정하고, 모델 문자열을 openrouter/로 시작한 뒤 그 뒤에 OpenRouter의 모델 id를 붙인다.
from litellm import completion
import os
os.environ["OPENROUTER_API_KEY"] = "sk-or-..."
resp = completion(
model="openrouter/openai/gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
접두사 구조를 뜯어보면 이해가 쉽다. 앞의 openrouter/는 LiteLLM에게 "OpenRouter를 통해 보내라"고 지시하고, 뒤의 openai/gpt-4o는 OpenRouter가 인식하는 모델 id다. 즉 openrouter/ 뒤에는 OpenRouter가 지원하는 어떤 모델 id든 그대로 올 수 있다.
이 조합의 실용적 이점은 분명하다. 직접 계약한 몇몇 공급자는 LiteLLM으로 직결하고, 그 외 실험적이거나 다양한 모델은 OpenRouter를 통해 붙이면서, 애플리케이션 코드는 전부 동일한 completion 호출로 유지할 수 있다. 모델을 늘리는 데 드는 통합 비용이 사실상 0에 수렴한다.
4. 스트리밍과 비동기
토큰을 실시간으로 흘려보내려면 stream=True만 추가하고 반환값을 순회하면 된다. 델타(delta)에서 조각을 꺼내 이어 붙이는 형태다.
from litellm import completion
resp = completion(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Write a haiku about the sea"}],
stream=True,
)
for chunk in resp:
print(chunk.choices[0].delta.content or "", end="")
delta.content가 None으로 오는 청크도 있으므로 or ""로 방어하는 것이 관례다. 이렇게 하면 빈 조각에서 예외가 나지 않는다.
비동기 코드에서는 acompletion을 쓴다. 시그니처는 completion과 같고, await만 붙이면 된다.
import asyncio
from litellm import acompletion
async def main():
resp = await acompletion(
model="anthropic/claude-3-5-sonnet-20241022",
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
asyncio.run(main())
스트리밍과 비동기는 조합해서 쓸 수 있다. FastAPI 같은 async 웹 프레임워크에서 토큰을 서버 전송 이벤트로 흘려보낼 때 이 두 가지를 함께 쓰게 된다.
5. Router — 로드 밸런싱과 폴백
호출량이 늘고 안정성이 중요해지면 SDK의 Router 클래스가 등장한다. Router는 여러 배포(deployment)를 하나의 별칭(alias)으로 묶어, 요청을 분산하고 실패를 자동으로 흡수한다.
먼저 model_list를 정의한다. 각 항목은 겉으로 보이는 이름 model_name과 실제 호출 파라미터 litellm_params를 가진다. 같은 model_name을 여러 배포에 붙이면, Router가 그 별칭으로 온 요청을 여러 배포에 나눠 보낸다.
from litellm import Router
model_list = [
{
"model_name": "my-gpt",
"litellm_params": {
"model": "openai/gpt-4o",
"api_key": "sk-...",
},
},
{
"model_name": "my-gpt",
"litellm_params": {
"model": "azure/gpt-4o-deployment",
"api_key": "...",
"api_base": "https://your-resource.openai.azure.com",
},
},
]
router = Router(model_list=model_list)
resp = router.completion(
model="my-gpt",
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
Router의 진짜 가치는 폴백(fallback)에 있다. 실패 종류에 따라 세 가지 폴백을 구분해서 설정할 수 있다.
| 폴백 종류 | 트리거 | 비고 |
|---|---|---|
fallbacks | 429/500 계열 오류 | 다른 모델로 넘김 |
context_window_fallbacks | 컨텍스트 초과 | enable_pre_call_checks=True 필요 |
content_policy_fallbacks | 콘텐츠 정책 거부 | 다른 모델로 우회 |
세 가지를 함께 건 예시는 다음과 같다. 컨텍스트 폴백을 쓰려면 사전 체크를 켜야 한다는 점만 주의하면 된다.
from litellm import Router
router = Router(
model_list=model_list,
fallbacks=[{"my-gpt": ["claude"]}],
context_window_fallbacks=[{"my-gpt": ["claude-long"]}],
content_policy_fallbacks=[{"my-gpt": ["safe-model"]}],
enable_pre_call_checks=True,
)
이 외에도 Router는 재시도(retries), 쿨다운(cooldowns), 타임아웃(timeouts)을 함께 관리한다. 한 배포가 계속 실패하면 잠시 쿨다운 목록에 넣어 트래픽을 보내지 않고, 일정 시간 뒤 복귀시키는 식이다. 애플리케이션 코드는 여전히 router.completion(model="my-gpt", ...) 한 줄이면 된다.
6. Proxy Server (AI Gateway)
여러 팀과 앱이 하나의 게이트웨이를 공유해야 한다면 Proxy Server가 답이다. Proxy는 별도 프로세스로 떠서, 어떤 OpenAI 호환 클라이언트든 받아들이는 게이트웨이 역할을 한다.
먼저 config.yaml에 model_list를 쓴다. API 키는 os.environ/로 환경 변수를 참조하게 하는 것이 안전하다.
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: claude
litellm_params:
model: anthropic/claude-3-5-sonnet-20241022
api_key: os.environ/ANTHROPIC_API_KEY
그다음 설정 파일을 지정해 프록시를 띄운다. 기본적으로 http://0.0.0.0:4000에서 대기한다.
litellm --config config.yaml
이제 아무 OpenAI 호환 클라이언트든 base URL을 프록시로 향하게 하고, 더미/베이스 키를 넣으면 그대로 동작한다. 실제 공급자 키는 프록시가 config에서 들고 있으므로 클라이언트에는 노출하지 않는다.
from openai import OpenAI
client = OpenAI(
base_url="http://0.0.0.0:4000",
api_key="anything", # 프록시가 관리하는 가상 키/더미 키
)
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)
Proxy 계층이 얹어주는 것은 단순 프록싱을 넘어선다. 팀별 가상 키(virtual keys) 발급, 예산(budgets) 한도, 비용 추적 대시보드, 요청 로깅이 모두 게이트웨이에서 중앙 관리된다. 애플리케이션은 진짜 공급자 키를 몰라도 되고, 운영팀은 누가 얼마나 썼는지 한곳에서 본다.
7. SDK vs Proxy, 그리고 실전 팁
둘 중 무엇을 쓸지는 규모로 갈린다.
- SDK를 쓸 때: 단일 앱이나 스크립트. 라이브러리를 import해서 코드 안에서 직접 호출하면 되고, 별도 인프라가 필요 없다. 개인 프로젝트, 배치 작업, 단일 서비스에 적합하다.
- Proxy를 쓸 때: 여러 팀 또는 여러 앱이 하나의 게이트웨이를 공유해야 할 때. 중앙 집중식 키 관리, 예산, 비용 대시보드가 필요하면 프록시가 정답이다. 조직 단위 LLM 플랫폼을 만들 때 자연스럽게 이 방향으로 간다.
실전에서 유용한 몇 가지를 정리한다.
- 응답 코드는 한 번만: 어떤 공급자를 부르든
resp.choices[0].message.content형태가 유지되므로, 응답 파싱 로직은 공급자마다 다시 짤 필요가 없다. - 키는 환경 변수로: SDK든 프록시 config든 키를 코드에 박지 말고 환경 변수로 주입한다. 프록시에서는
os.environ/참조를 쓴다. - 모델 문자열이 곧 스위치: 공급자 전환은 접두사 교체와 환경 변수 설정, 이 두 가지가 전부다. 배포 환경별로 모델 문자열만 바꾸는 전략이 잘 통한다.
- OpenRouter를 옆에 둔다: 자주 쓰는 공급자는 직결하고, 실험적이거나 다양한 모델은
openrouter/접두사로 붙이면 통합 비용 없이 선택지를 넓힐 수 있다. - 작게 시작해서 올린다: 처음엔 SDK로 시작하고, 팀이 늘고 중앙 관리가 필요해지는 시점에 같은
model_list개념을 그대로 Proxy config로 옮기면 된다.
정리하면, LiteLLM은 공급자 파편화를 코드 밖으로 밀어낸다. 첫 호출은 몇 줄이면 되고, 필요해지는 순간 Router로 안정성을, Proxy로 조직 차원의 통제를 더할 수 있다. 더 깊은 내부 동작과 프로덕션 세부 설정은 LiteLLM 완전 가이드에서 이어서 보면 된다.