LiteLLM 타임아웃: 응답이 오지 않는 호출 다루기
LiteLLM 타임아웃: 응답이 오지 않는 호출 다루기
LLM 호출에서 가장 골치 아픈 상황 중 하나가 '응답은 오는데 언제 올지 모르는' 경우예요. 제공자가 느리게 반응하거나, 스트리밍에서 첫 토큰이 영영 안 오거나, 연결이 조용히 끊겨 버리기까지 하죠. 이때 무한정 기다리게 두면 요청 슬롯이 계속 잡혀서 서버 전체가 막힐 수 있어요. LiteLLM 타임아웃 설정은 바로 이 '응답을 얼마나 기다릴지'의 기준을 정해 주는 도구입니다.
기본 원칙 하나를 먼저 짚을게요. 라우터에 설정한 timeout은 호출 전체 길이를 위한 값이고, completion() 호출 레벨에도 그대로 전달됩니다.
출처: 공식문서 — Timeouts
모델별 timeout과 stream_timeout
각 모델의 litellm_params 아래에서 timeout과 stream_timeout을 따로 설정할 수 있어요. 두 값이 맡는 역할은 다릅니다.
timeout— 전체 응답을 받을 때까지의 최대 시간. 오래 걸리는 완성(completion) 요청을 제한할 때 써요.stream_timeout— 스트리밍 응답에서 첫 번째 청크(첫 토큰) 를 기다리는 최대 시간. 한가한 제공자(예: Bedrock 느린 시작)에 걸린 요청을 중단하고 다른 모델로 재시도할 때 유용해요.
아래 예시는 모델 배포에 5분 타임아웃과 30초 스트림 타임아웃을 지정한 설정입니다.
from litellm import Router
import asyncio
model_list = [
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/chatgpt-v-2",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
"timeout": 300, # 5분 타임아웃
"stream_timeout": 30, # 스트리밍 호출 30초 제한
}
}
]
router = Router(model_list=model_list, routing_strategy="least-busy")
async def router_acompletion():
response = await router.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hey, how's it going?"}],
)
return response
asyncio.run(router_acompletion())
timeout과 stream_timeout은 요청이 '얼마나 오래 실행될 수 있는지'를 제한해요. 그런데 여기에는 별개의 문제가 하나 있어요. 프록시 앞에 놓인 로드밸런서나 리버스 프록시는 클라이언트가 정상적으로 응답을 기다리는 중이어도 '유휴해 보이는' 연결을 닫아버리는 경우가 많습니다. 특히 첫 토큰 전에 말 없는 공백이 긴 모델(확장·적응형 추론 모델, 전반적으로 느린 제공자)은 콘텐츠가 도착하기 전에 이 유휴 타임아웃에 걸릴 수 있어요.
keepalive_seconds: 유휴 연결 유지하기
그 공백 동안 연결을 살려 두기 위해, 배포의 litellm_params에 keepalive_seconds를 설정할 수 있어요. 스트림이 keepalive_seconds보다 오래 조용해지면, 프록시가 연결로 SSE 주석 프레임(: ping)을 보내고, 실제 콘텐츠가 재개될 때까지 keepalive_seconds 간격으로 반복합니다. 주석 프레임은 SSE 스펙의 일부라 클라이언트와 중간 프록시가 무시하도록 되어 있어, 애플리케이션이 보는 응답에는 영향을 주지 않아요.
model_list:
- model_name: claude-opus
litellm_params:
model: anthropic/claude-opus-4-8
api_key: os.environ/ANTHROPIC_API_KEY
keepalive_seconds: 15
클라이언트가 아무 때나 하트비트를 켤 수 있게 두면, 유휴해 보이는 스트림을 로드밸런서 타임아웃 너머로 무한정 붙들어 max_parallel_requests 슬롯을 의도보다 오래 점유할 수 있어요. 그래서 keepalive_seconds는 기본적으로 운영자만 설정할 수 있습니다. 클라이언트의 요청 레벨 keepalive_seconds는 배포가 allow_client_keepalive_override: true를 켜지 않으면 무시돼요.
model_list:
- model_name: claude-opus
litellm_params:
model: anthropic/claude-opus-4-8
api_key: os.environ/ANTHROPIC_API_KEY
keepalive_seconds: 15
allow_client_keepalive_override: true
오버라이드가 허용되면, 요청이 배포 기본값을 바꿀 수 있어요. 명시적 0으로 끄는 것도 가능하고, 배포 레벨 keepalive_seconds: 0은 오버라이드 권한을 줘도 우선하는 강제 비활성화입니다. 유효값은 1~300초로 제한(클램프)돼요.
실제 값은 1~300초로 제한됩니다. 요청 본문 필드 대신 x-litellm-keepalive-seconds 헤더로도 설정할 수 있는데, 이 헤더도 본문 필드와 같은 allow_client_keepalive_override 게이트를 거쳐요.
요청별 타임아웃
호출 하나마다 별도의 timeout을 줄 수도 있어요. 라우터 기본값에 의존하지 않고 특정 요청에만 제한을 걸고 싶을 때 유용합니다.
from litellm import Router
router = Router(model_list=model_list)
response = router.completion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "what color is red"}],
timeout=1,
)
프록시 HTTP 요청에서도 mock_timeout: true를 보내면 타임아웃 동작을 테스트할 수 있어요. 이 플래그는 keepalive_seconds와 달리 요청 필드로 바로 동작합니다. 재시도/폴백 로직이 타임아웃 상황을 제대로 처리하는지 확인할 때 쓰면 돼요.
curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-1234' \
--data-raw '{
"model": "gemini/gemini-1.5-flash",
"messages": [{"role": "user", "content": "hi"}],
"mock_timeout": true
}'
mock_timeout은 현재 /chat/completions와 /completions 엔드포인트에서만 지원됩니다.