엔드포인트 플러그인
엔드포인트 플러그인 (Endpoint Plugins)
엔드포인트 플러그인은 트리 밖(out-of-tree) 패키지가 OpenAI 호환 API 서버에 HTTP 라우트를 추가할 수 있게 해주는 기능이에요. 이 플러그인의 범위는 HTTP 표면만이에요. 즉 라우트를 등록하고, 필요하면 그 라우트가 쓰는 앱별 상태를 등록하는 일까지예요. 플러그인은 시작 시 받은 EngineClient를 통해(예: engine_client.collective_rpc(...)) 트리 안 서빙 핸들러와 똑같은 방식으로 엔진에 도달해요. 즉 새 엔진 접근 경로가 추가되는 게 아니에요.
!!! warning "보안" 엔드포인트 플러그인은 기본으로 로드되지 않고 명시적으로 allowlist에 넣어야 해요. 활성화하기 전에 Endpoint Plugins 보안 상태를 읽어야 하고, 특히 route shadowing 경고를 꼭 확인하세요.
EndpointPlugin 프로토콜
엔드포인트 플러그인은 런타임 체크 가능한 Protocol인 EndpointPlugin을 구현해요.
class EndpointPlugin(Protocol):
name: str
required_tasks: tuple[SupportedTask, ...] | None
def attach_router(self, app: FastAPI) -> None: ...
async def init_state(
self, engine_client: EngineClient | None, state: State, args: Namespace
) -> None: ...
name: 로그와VLLM_PLUGINSallowlisting에 쓰는 고유 식별자required_tasks: 이 플러그인이 로드되려면 서버가 지원해야 하는 작업들.None이면 작업 요구사항 없음attach_router:app에 라우트를 등록init_state: 요청 시간에 라우트가 읽는 앱별 상태를 초기화
두 단계 수명주기
라우트는 엔진이 존재하기 전에 등록돼요. 그래서 인터페이스는 서버 시작의 서로 다른 두 지점에서 실행되는 두 훅을 노출해야 해요.
| Phase | Called from | engine_client available? |
Work |
|---|---|---|---|
| A. Route registration | build_app() |
No | attach_router(app)로 라우트 추가. 여기서 엔진을 건드리지 말 것. |
| B. State init | init_app_state() |
보통은 있지만 CPU 전용 렌더 서버에선 None |
init_state(engine_client, state, args)로 engine_client를 들고 있는 서빙 핸들러를 만들고 state에 저장 |
app.state가 init_app_state()에 전달되는 state 객체 자체이기 때문에, A 단계에서 저장한 객체는 B 단계에서 보이고, B 단계에서 저장한 객체는 요청 시간에 request.app.state로 라우트 핸들러가 볼 수 있어요. 트리 안 엔드포인트가 이미 쓰는 패턴과 같아요.
엔진이 없는 서버 (렌더 서버)
CPU 전용 렌더 서버(init_render_app_state())는 EngineClient가 없어요. 그래도 render 작업에 적합한 플러그인(required_tasks가 None이거나 "render" 포함)이면 두 단계 모두 실행돼요. attach_router는 평소처럼 호출되지만 init_state는 engine_client=None으로 호출돼요.
엔진이 필요한 플러그인이라면 두 가지 선택지가 있어요.
required_tasks에서"render"를 제외해서 렌더 서버에서 애초에 로드되지 않게render에 로드되는 걸 받아들이고,init_state나 라우트 핸들러에서None을 확인해 존재하지 않는 클라이언트 역참조 대신 오류 응답(예: HTTP 503)을 반환
tests/plugins/vllm_add_dummy_endpoint_plugin이 두 번째 방식을 보여줘요. 그 라우트 핸들러는 state.dummy_engine_client가 None일 때 503을 반환해요.
라우트 핸들러에서 엔진에 도달하기
init_state가 플러그인이 engine_client를 작은 서빙 핸들러로 잡아서 state에 저장하는 곳이에요. attach_router에서 추가한 라우트는 요청 시간에 request.app.state에서 그 핸들러를 읽고, 보통 engine_client.collective_rpc(...)로 그 핸들러를 통해 엔진을 호출해요.
이 최소 예시는 required_tasks가 None이라 앞 섹션의 None 검사를 생략했어요. 이 예시는 실제로 render에 적합하므로, 배포 전에 tests/plugins/vllm_add_dummy_endpoint_plugin처럼 engine_client=None을 처리해야 해요.
from fastapi import FastAPI, Request
class MyAdminEndpointPlugin:
name = "my_admin_endpoint_plugin"
required_tasks: tuple[str, ...] | None = None
def attach_router(self, app: FastAPI) -> None:
@app.get("/plugins/my_admin_endpoint_plugin/scheduler_config")
async def scheduler_config(raw_request: Request):
engine_client = raw_request.app.state.my_engine_client
results = await engine_client.collective_rpc("get_scheduler_config")
return {"scheduler_config": results}
async def init_state(self, engine_client, state, args) -> None:
state.my_engine_client = engine_client
이 예시의 완전하고 테스트된 버전은 tests/plugins/vllm_add_dummy_endpoint_plugin에 있고, tests/plugins_tests/test_endpoint_plugins.py에서 e2e(실제 HTTP 요청 포함)로 검증돼요.
entry point 등록
vllm.endpoint_plugins 그룹에 인자가 없는 팩토리(클래스나 함수)를 등록해요. 팩토리는 EndpointPlugin을 만족하는 객체를 반환해야 해요.
# pyproject.toml
[project.entry-points."vllm.endpoint_plugins"]
my_admin_api = "my_pkg.endpoints:MyAdminEndpointPlugin"
# setup.py equivalent
setup(
name="my_pkg",
entry_points={
"vllm.endpoint_plugins": [
"my_admin_api = my_pkg.endpoints:MyAdminEndpointPlugin"
]
},
)
entry point 이름(위에서 my_admin_api)은 플러그인의 name 속성과 독립적이에요. VLLM_PLUGINS allowlisting은 vllm.general_plugins와 같은 규칙(플러그인 시스템 참고)에 따라 entry point 이름으로 매칭돼요.
게이팅: VLLM_PLUGINS와 required_tasks
엔드포인트 플러그인은 다른 플러그인 그룹의 로더보다 더 엄격한 load_endpoint_plugins가 발견·게이팅해요.
VLLM_PLUGINS가 설정되고 플러그인을 지명하지 않으면 아무것도 로드되지 않아요. 다른 플러그인 그룹은VLLM_PLUGINS가 집합을 좁히지 않으면 전부 로드하는데, 엔드포인트 플러그인은 네트워크에 노출되는 표면을 추가하기 때문에 그 기본값이 뒤집혀요. 보안 참고.required_tasks가 서버의 지원 작업과 교집합이 있어야 해요(None이 아니라면). pooling 전용 배포처럼 서비스할 수 없는 서버에 플러그인이 라우트를 다는 걸 막는 데 써요.- 인스턴스화 중 문제를 일으키는 팩토리는 로그로 남기고 스킵돼요. 서버 시작을 중단하지 않아요.
엔드포인트 플러그인을 로드하는 건 오직 프런트엔드 API 서버 프로세스뿐이에요. 워커나 엔진 코어 프로세스는 가드할 필요가 없어요.
vllm.general_plugins와의 결합
엔드포인트 플러그인은 HTTP 표면만 다뤄요. 플러그인이 새 엔진 사이드 동작(새 워커 사이드 RPC 메서드, 커스텀 통계)도 필요하다면, 그 절반은 워커 프로세스에서 로드되는 기존 vllm.general_plugins 그룹을 통해 따로 배포해요(플러그인 시스템 참고). 두 entry point는 독립적으로 등록·로드돼요. 어느 하나가 다른 하나를 함의하지 않아요. 권장 배포 형태는 두 가지를 모두 노출하는 단일 패키지예요.
[project.entry-points."vllm.general_plugins"]
my_admin_engine = "my_pkg.engine:register" # adds the worker side method
[project.entry-points."vllm.endpoint_plugins"]
my_admin_api = "my_pkg.endpoints:MyAdminEndpointPlugin" # adds the HTTP route
단일 엔드포인트 플러그인이 엔진/워커 상태까지 바꿀 거라 기대하지 마세요. 라우트에 아직 존재하지 않는 워커 사이드 메서드가 필요하면, 짝이 되는 general_plugins entry point로 추가하세요.
경로-접두사 규칙
지금은 라우트 충돌 강제가 없어요(RFC #46565 추적). 플러그인의 attach_router는 핵심 라우트와 충돌하는 경로를 등록할 수 있고, 나중에 붙은 라우트가 이겨요. 운영자를 놀라게 하지 않으려면:
- 라우트를 뚜렷한 접두사 아래에 네임스페이스하세요(예:
/plugins/<plugin-name>/...)./v1/...같은 핵심 접두사를 재사용하지 말 것 - 기존 동작을 재정의하거나 확장하려는 경우에만 핵심 접두사 아래(예: 예시의
/v1/admin/scheduler_config) 라우트를 등록하고, 그 의도를 플러그인을 allowlist하는 운영자에게 명확히 문서화하세요
호환성
state/서빙 핸들러 내부(예: 트리 안 OpenAIServing* 클래스의 모양)는 아직 안정적인 공개 계약이 아니에요. 자기 책임 하에 쓰고 vLLM 버전 사이에 바뀔 수 있다고 생각하세요. FastAPI, EngineClient, EndpointPlugin 프로토콜 자체가 지원되는 표면이에요.
더 알아보기 (Learn more)
- vLLM 공식 문서: Endpoint Plugins
- 관련 문서: Plugin System, 보안 (Endpoint Plugins)