본문 바로가기
AI Agent/Frameworks

LiteLLM (1) - 전체 구조와 SDK

by AtoN 2024. 8. 29.

LiteLLM 전체 구조와 Python SDK

LLM을 여러 provider와 연결하기 시작하면 API마다 모델 이름, 인증 방식, 요청 parameter, 응답 구조와 error type이 조금씩 달라집니다. OpenAI에서 Anthropic이나 Bedrock으로 모델을 바꾸는 것만으로도 애플리케이션 코드 여러 곳이 provider별 구현을 알아야 할 수 있습니다.

LiteLLM은 이런 차이를 하나의 공통 호출 계층 뒤로 숨기는 LLM abstraction이자 AI Gateway입니다. 애플리케이션 안에서 Python SDK로 직접 사용할 수도 있고, 여러 서비스가 함께 접근하는 Proxy Server를 별도의 AI Gateway로 배포할 수도 있습니다. 두 방식은 배치 위치는 다르지만 provider마다 다른 요청과 응답을 공통 인터페이스로 다룬다는 같은 기반 위에서 동작합니다.

그 아래에는 provider별 Translation Layer가 있습니다. LiteLLM이 받은 공통 형식의 요청을 OpenAI, Anthropic, Bedrock, Vertex AI 같은 실제 provider API에 맞게 변환하고, 돌아온 응답과 오류를 다시 공통 형태로 정규화합니다. Python SDK의 completion() 같은 호출도 이 계층을 거쳐 실제 provider와 통신합니다.

여러 실제 deployment를 하나의 논리적인 model group으로 운영해야 한다면 Router를 사용할 수 있습니다. Router는 요청을 어느 deployment로 보낼지 선택하고 load balancing, retry, fallback, cooldown 같은 동작을 관리합니다.

Application
    ↓
Python SDK
    ├─ 직접 provider 호출
    └─ Router를 통한 deployment 선택
            ↓
Provider Translation
            ↓
OpenAI / Anthropic / Bedrock / Vertex AI / ...

여러 애플리케이션과 팀이 같은 LLM 인프라를 공유해야 한다면 이 호출 계층을 Proxy Server로 중앙화할 수 있습니다. Proxy는 내부의 LiteLLM 호출 계층과 Router를 사용하면서, 그 앞에 virtual key, authentication, rate limiting, budget과 spend tracking, logging, guardrail 같은 gateway 기능을 추가합니다.

Applications / Teams
        ↓
LiteLLM Proxy
authentication / rate limit / budget / logging
        ↓
Router
deployment selection / retry / fallback
        ↓
LiteLLM 호출 계층
        ↓
Provider Translation
        ↓
LLM Provider API

따라서 LiteLLM을 이해할 때 completion() 함수부터 외우기보다 provider 차이를 흡수하는 SDK, 여러 deployment를 선택하는 Router, 호출 정책을 중앙화하는 Proxy라는 세 층을 먼저 잡는 편이 좋습니다.

먼저 Python SDK의 가장 작은 호출에서 이 추상화가 어떻게 보이는지 살펴보고, 요청·응답·오류가 provider-independent interface로 바뀌는 과정을 따라갑니다. 이어서 Chat Completions와 Responses API의 차이, streaming과 async, provider별 parameter 차이와 비용 정보까지 연결해서 봅니다.


1장. LiteLLM의 전체 구조

LiteLLM은 크게 두 위치에서 사용할 수 있습니다.

직접 통합

애플리케이션
    │
    ▼
Python SDK
    │
    ├── Router (필요한 경우)
    │
    ▼
Provider Translation
    │
    ▼
OpenAI / Anthropic / Gemini / Bedrock / ...


중앙 게이트웨이

서비스 A ──┐
서비스 B ──┼──► LiteLLM Proxy
서비스 C ──┘          │
                       ▼
                     Router
                       │
                       ▼
              Provider Translation
                       │
                       ▼
                    Model API

LiteLLM 공식 프로젝트도 Python SDK로 애플리케이션에 직접 통합하는 방식과 Proxy Server를 중앙 AI Gateway로 배포하는 방식을 함께 제공합니다. SDK와 Proxy는 서로 경쟁하는 제품이라기보다 같은 provider abstraction을 서로 다른 위치에 배치하는 방법에 가깝습니다.

Python SDK는 애플리케이션 프로세스 안에서 동작합니다. 각 provider의 SDK를 직접 호출하는 대신 LiteLLM의 공통 호출 형식을 사용하고, 실제 provider에 필요한 변환은 내부에서 처리합니다.

여러 deployment를 묶어 운영해야 하면 SDK 계층에서 Router를 사용할 수 있습니다. Router는 같은 논리적 모델에 연결된 여러 deployment 중 하나를 선택하고, 실패 시 retry나 fallback을 적용하는 역할을 맡습니다.

Proxy는 이 호출 경계를 애플리케이션 밖으로 옮깁니다. 여러 서비스가 Proxy 하나를 호출하고 실제 provider credential과 routing policy를 gateway가 관리합니다.

그래서 대략 다음 기준으로 역할을 나눌 수 있습니다.

Provider API 차이를 숨긴다
    → Python SDK / Translation Layer

여러 deployment 중 실행 대상을 고른다
    → Router

여러 서비스의 호출 정책을 중앙화한다
    → Proxy

SDK부터 보는 이유는 SDK가 LiteLLM의 유일한 시작점이기 때문이 아니라, Router와 Proxy 아래에서도 반복되는 요청 변환과 응답 정규화를 가장 직접적으로 확인할 수 있기 때문입니다.


2장. completion()으로 첫 호출

Chat Completions 형태의 가장 기본적인 호출은 completion()입니다.

from litellm import completion

response = completion(
    model="openai/gpt-4o",
    messages=[
        {
            "role": "user",
            "content": "대한민국의 수도는 어디인가요?",
        }
    ],
)

print(response.choices[0].message.content)

LiteLLM에서는 흔히 다음처럼 provider prefix와 model을 함께 지정합니다.

provider/model

예를 들어 공식 예제에서도 다음과 같은 형태를 사용합니다.

completion(
    model="anthropic/claude-sonnet-4-20250514",
    messages=[
        {
            "role": "user",
            "content": "안녕하세요",
        }
    ],
)

Local OpenAI-compatible endpoint나 Ollama처럼 endpoint를 직접 지정해야 하는 경우에는 api_base 같은 설정이 추가될 수 있습니다.

completion(
    model="ollama/llama3",
    messages=[
        {
            "role": "user",
            "content": "안녕하세요",
        }
    ],
    api_base="http://localhost:11434",
)

다만 provider/model을 모든 LiteLLM 호출에 반드시 적용되는 유일한 문법으로 이해할 필요는 없습니다. Router나 Proxy configuration에서 provider가 이미 결정되어 있거나 특정 API가 모델 이름을 별도로 해석하는 경우도 있습니다.

중요한 것은 문자열 형식 자체보다 LiteLLM이 어떤 provider의 어떤 model을 호출해야 하는지 식별할 수 있다는 것입니다.

Provider마다 실제 인증 방법과 model ID는 다르지만 애플리케이션에서는 messages, temperature, tools 같은 공통 parameter를 비슷한 호출 구조로 사용할 수 있습니다.

이 추상화의 장점은 provider 선택이 애플리케이션의 비즈니스 로직 전체로 퍼지는 것을 줄일 수 있다는 데 있습니다.


3장. 응답과 오류도 공통 형태로 받는다

요청 형식만 통일하면 provider abstraction은 절반만 완성된 것입니다. 응답과 오류까지 공통 형태로 다룰 수 있어야 호출부에서 provider별 분기를 줄일 수 있습니다.

completion()의 반환값은 OpenAI Chat Completions와 유사한 ModelResponse 형태로 정규화됩니다.

text = response.choices[0].message.content

print(response.usage.prompt_tokens)
print(response.usage.completion_tokens)
print(response.usage.total_tokens)

예를 들어 Anthropic API를 호출하더라도 애플리케이션이 provider 원본 응답의 input_tokens, output_tokens 같은 필드 구조를 모든 호출 지점에서 직접 처리할 필요가 줄어듭니다.

오류도 같은 원리로 정규화됩니다.

import litellm
from litellm import completion


try:
    completion(
        model="anthropic/claude-sonnet-4-20250514",
        messages=[
            {
                "role": "user",
                "content": "안녕하세요",
            }
        ],
    )

except litellm.AuthenticationError:
    print("인증 정보 확인")

except litellm.RateLimitError:
    print("레이트리밋 처리")

except litellm.APIError:
    print("그 밖의 API 오류")

Provider별 오류를 공통 exception type으로 바꾸면 애플리케이션이나 Router가 provider 이름을 일일이 확인하지 않고 오류의 의미에 따라 retry와 fallback 정책을 적용하기 쉬워집니다.

OpenAI rate limit
Anthropic rate limit
Bedrock throttling
        ↓
공통 exception mapping
        ↓
RateLimit 계열 정책

이 점이 이후 Router의 retry 정책과 직접 연결됩니다. Router가 모든 오류에 똑같이 다시 시도하는 것이 아니라, rate limit과 timeout은 retry하고 authentication이나 invalid request는 다른 방식으로 처리하도록 구성할 수 있는 기반입니다.


4장. Chat Completions와 Responses API

LiteLLM의 LLM 호출이 모두 completion() 하나로 끝나는 것은 아닙니다.

일반적인 message 기반 chat 호출에는 completion()을 사용할 수 있습니다.

from litellm import completion

response = completion(
    model="openai/gpt-4o",
    messages=[
        {
            "role": "user",
            "content": "이 문장을 요약해줘",
        }
    ],
)

반면 OpenAI Responses API처럼 reasoning, richer input item, built-in tool 같은 별도의 API surface를 사용하는 기능은 responses()로 다룰 수 있습니다.

현재 LiteLLM의 responses() 구현 시그니처는 Responses API와 유사하게 input과 reasoning을 받습니다.

from litellm import responses

response = responses(
    model="gpt-5-mini",
    input=[
        {
            "role": "user",
            "content": "이 문제를 분석해줘",
        }
    ],
    reasoning={
        "effort": "medium",
    },
)

현재 LiteLLM 공식 시작 문서에는 다음과 같은 compatibility 형태의 예시도 남아 있습니다.

responses(
    model="gpt-5-mini",
    messages=[
        {
            "role": "user",
            "content": "이 문제를 분석해줘",
        }
    ],
    reasoning_effort="medium",
)

하지만 현재 구현의 직접적인 responses() interface는 input과 reasoning을 사용하므로, 새 코드에서 Responses API 자체의 구조를 학습하려면 input / reasoning 형태로 이해하는 편이 더 명확합니다. 공식 시작 문서와 현재 구현 사이에는 이 부분의 표현 차이가 있습니다.

두 API를 단순한 신버전과 구버전 관계로 보면 안 됩니다.

completion()
    Chat Completions 계열 interface

responses()
    Responses API 계열 interface

Provider와 model에 따라 어느 endpoint와 기능을 지원하는지가 다를 수 있고, LiteLLM 내부에서는 Responses 요청을 provider의 native Responses API로 보내거나 필요한 경우 다른 provider interface와 연결하기 위한 transformation을 수행하기도 합니다.

따라서 새 기능을 붙일 때는 먼저 사용하려는 model과 provider가 어느 API surface에서 필요한 기능을 지원하는지 확인하는 편이 안전합니다.


5장. Streaming과 비동기 호출

서버 애플리케이션에서는 한 요청이 끝날 때까지 thread나 request handler가 계속 기다리는 방식만으로는 동시 요청을 효율적으로 처리하기 어려울 수 있습니다.

LiteLLM은 동기 호출과 비동기 호출을 모두 제공합니다.

from litellm import acompletion

response = await acompletion(
    model="openai/gpt-4o",
    messages=[
        {
            "role": "user",
            "content": "안녕하세요",
        }
    ],
)

Streaming은 Chat Completions 계열에서 stream=True로 사용할 수 있습니다.

from litellm import completion

stream = completion(
    model="openai/gpt-4o",
    messages=[
        {
            "role": "user",
            "content": "짧은 이야기를 써줘",
        }
    ],
    stream=True,
)

for chunk in stream:
    print(
        chunk.choices[0].delta.content or "",
        end="",
    )

Provider마다 원래의 streaming event 구조는 다를 수 있지만 LiteLLM은 호출부가 가능한 한 공통 chunk 형태를 사용하도록 변환합니다.

다만 추상화가 provider의 실제 동작 차이까지 없애는 것은 아닙니다.

예를 들어 다음 항목은 provider나 endpoint에 따라 실제 동작이 달라질 수 있습니다.

첫 token이 도착하는 시점
usage가 전달되는 시점
tool call streaming 방식
reasoning event 표현
stream 종료 event

따라서 streaming latency가 중요한 서비스라면 추상화 계층만 보고 판단하기보다 실제 model과 endpoint 조합으로 TTFT, inter-token latency, usage 집계까지 측정하는 것이 좋습니다.


6장. 공통 parameter와 model capability는 다른 문제다

공통 interface가 있다고 해서 모든 model이 같은 parameter와 기능을 지원하는 것은 아닙니다.

예를 들어 다음 호출이 있다고 해 보겠습니다.

completion(
    model="openai/gpt-4o",
    messages=messages,
    temperature=0,
    tools=tools,
    max_tokens=500,
)

다른 provider의 model로 교체했을 때 일부 parameter는 그대로 사용할 수 있지만, 어떤 model은 특정 parameter를 지원하지 않거나 의미가 다를 수 있습니다.

LiteLLM의 역할은 provider별 parameter를 가능한 범위에서 공통 OpenAI-style parameter에 연결하고 실제 provider parameter로 변환하는 것입니다.

공통 parameter
temperature
tools
max_tokens
...

        ↓ Translation

Provider A parameter
Provider B parameter
Provider C parameter

하지만 LiteLLM이 model 자체의 capability를 새로 만들어 주는 것은 아닙니다.

LiteLLM이 제공하는 것
    호출 interface 정규화
    parameter mapping
    response normalization

LiteLLM이 없앨 수 없는 것
    model capability 차이
    provider feature 차이
    context window 차이
    tool / reasoning 지원 차이

따라서 LiteLLM을 “어떤 model이든 완전히 같은 기능으로 교체할 수 있게 만드는 라이브러리”라고 이해하면 안 됩니다.

실제 코드에서는 get_supported_openai_params()처럼 provider와 model이 지원하는 공통 parameter를 확인하는 기능을 사용할 수 있고, unsupported parameter를 어떻게 처리할지 drop_params 같은 설정도 함께 고려할 수 있습니다.

핵심은 parameter 이름을 통일하는 것과 capability를 통일하는 것을 구분하는 것입니다.


7장. 모델 정보와 비용 계산

LiteLLM 저장소에는 모델별 가격, context window와 capability 관련 정보를 담는 model_prices_and_context_window.json이 있습니다. 현재 repository에서도 이 파일이 별도의 registry로 관리됩니다.

LiteLLM은 기본적으로 이 model cost map을 이용해 모델 정보와 token usage를 연결할 수 있습니다. 현재 코드에서는 기본 model cost map URL도 GitHub의 model_prices_and_context_window.json을 가리키고 있습니다.

애플리케이션 관점에서는 다음과 같은 용도로 활용할 수 있습니다.

모델의 context 관련 정보 확인
        +
호출 결과의 token usage
        +
모델별 input / output 단가
        ↓
예상 비용 계산과 tracking

하지만 이 registry를 provider의 공식 billing ledger와 같은 것으로 이해하면 안 됩니다.

모델 출시나 regional model ID 추가가 registry보다 먼저 일어날 수도 있고, 아직 매핑되지 않은 model에서는 가격이나 capability 정보가 불완전할 수 있습니다. 실제 repository에서도 새로운 model이나 Bedrock regional model mapping이 누락되는 문제가 계속 보고됩니다.

따라서 가격 레지스트리는 운영과 추정에 유용한 metadata source로 사용하되, 실제 청구 금액 검증이 중요한 시스템에서는 provider의 billing data와 대조하는 편이 안전합니다.

모델이나 가격의 개수를 외우는 것보다 외부 model 정보를 별도 registry에서 관리하고 계속 갱신하는 구조를 이해하는 것이 더 중요합니다.


8장. SDK, Router, Proxy 중 어디서 시작할까

LiteLLM의 SDK, Router, Proxy는 같은 수준에서 경쟁하는 세 제품이 아닙니다.

요구사항이 커지면서 추상화 계층을 하나씩 추가하는 방식으로 이해하면 쉽습니다.

상황 먼저 볼 계층
Python 애플리케이션 하나에서 여러 provider 호출 SDK
같은 논리 모델에 여러 deployment가 있음 SDK + Router
rate limit이나 장애 시 다른 deployment로 전환 Router
여러 서비스가 같은 모델 정책을 공유 Proxy
팀별 key·budget·rate limit이 필요 Proxy
provider 하나만 사용하고 abstraction이 필요 없음 Provider SDK 직접 사용도 가능

예를 들어 애플리케이션 하나가 OpenAI와 Anthropic 모델을 선택적으로 호출하는 정도라면 Python SDK만으로 충분할 수 있습니다.

Application
    ↓
LiteLLM SDK
    ↓
Provider

같은 model group을 여러 region이나 account에 배포했고 availability와 load balancing이 중요해진다면 Router를 추가할 수 있습니다.

Application
    ↓
Router
    ├─ Deployment A
    ├─ Deployment B
    └─ Deployment C

여러 팀이 같은 정책을 적용받아야 한다면 Proxy가 자연스럽습니다.

Team A ─┐
Team B ─┼─→ Proxy → Router → Providers
Team C ─┘

따라서 처음부터 Proxy를 배포하는 것이 항상 더 좋은 설계는 아닙니다. 문제가 provider abstraction인지, deployment routing인지, 조직 차원의 gateway policy인지를 먼저 구분하는 편이 좋습니다.


9장. 오픈소스와 Enterprise 라이선스 경계

LiteLLM을 상업적으로 사용할 때는 repository 전체를 하나의 라이선스로 단순화해서 보면 안 됩니다.

현재 repository의 루트 LICENSE는 enterprise/ 디렉터리의 코드는 해당 디렉터리의 별도 라이선스를 따르고, 그 밖의 코드는 MIT License로 제공한다고 명시합니다.

Repository
    ├─ enterprise/
    │      → 별도 Enterprise License
    │
    └─ 그 밖의 코드
           → MIT License

따라서 OSS SDK나 Proxy의 일반 기능을 사용하는 것과 Enterprise 기능을 사용하는 것은 라이선스 조건이 같다고 가정하면 안 됩니다.

상용 도입에서는 단순히

"LiteLLM = MIT"

라고 판단하기보다 실제 사용하려는 코드와 기능이 어느 경로와 라이선스에 속하는지 확인해야 합니다.

공식 repository 역시 Enterprise 기능을 별도의 Commercial License 영역으로 설명하고 있습니다.


10장. LiteLLM에서 가장 중요한 경계

LiteLLM의 핵심을 단순히 "여러 LLM을 한 함수로 부르는 라이브러리"라고 보면 전체 구조를 놓치기 쉽습니다.

가장 아래에서는 provider별 차이를 Translation Layer가 흡수합니다.

공통 request
    ↓
Provider Translation
    ↓
Provider API

Provider response
    ↓
Response / Exception normalization
    ↓
공통 result

그 위에 Router를 놓으면 하나의 model이 아니라 여러 deployment 중 어디에서 실행할지를 결정할 수 있습니다.

Logical Model
    ↓
Router
    ├─ Deployment A
    ├─ Deployment B
    └─ Deployment C

그리고 Proxy를 두면 이 호출 구조를 중앙 서비스로 만들고 인증, budget, rate limit, logging 같은 조직 단위 정책을 적용할 수 있습니다.

Applications
    ↓
Proxy
    ↓
Router
    ↓
Translation
    ↓
Providers

그래서 세 계층의 역할은 다음처럼 구분할 수 있습니다.

Translation
    → Provider 차이를 어떻게 흡수할 것인가

Router
    → 어느 deployment에서 실행할 것인가

Proxy
    → 누가 어떤 정책으로 모델을 사용할 수 있는가

Python SDK의 completion()은 이 구조를 가장 작은 단위에서 확인할 수 있는 진입점입니다. 일반적인 Chat Completions 호출은 completion()으로 처리하고, Responses API 계열 기능이 필요하면 responses()라는 별도 interface를 사용할 수 있습니다.

공통 interface가 provider의 모든 차이를 없애는 것은 아닙니다. 모델마다 지원 기능과 parameter, streaming 방식과 성능 특성이 다르고, 비용 registry도 provider billing의 절대적인 원장은 아닙니다.

LiteLLM을 실무에서 사용할 때 중요한 것은 함수 이름을 외우는 것이 아니라 어느 차이는 추상화할 수 있고, 어느 차이는 provider와 model의 실제 capability로 남는지 경계를 이해하는 것입니다.


용어 정리

용어 뜻
LiteLLM Python SDK 여러 LLM provider를 공통 interface로 호출하기 위한 Python 계층
completion() Chat Completions 형태의 기본 호출 함수
acompletion() completion()의 비동기 호출 함수
responses() Responses API 형태의 입력과 기능을 다루는 호출 interface
ModelResponse Chat Completions 결과를 공통 형태로 표현하는 응답 객체
Translation Layer 공통 요청을 provider 요청으로 바꾸고 응답을 다시 정규화하는 계층
Router 여러 deployment 중 실행 대상을 선택하고 retry·fallback·load balancing을 관리하는 계층
Proxy Server 인증·budget·rate limit·logging 등의 정책을 중앙에서 적용하는 AI Gateway
provider/model provider와 model을 함께 나타내는 대표적인 LiteLLM model 식별 형태
Exception Mapping provider별 오류를 공통 exception 계층으로 정규화하는 과정
Model Cost Map 모델별 가격과 context 관련 metadata를 관리하는 registry

참고자료

댓글