/v1/chat/completions 엔드포인트를 제공하고, 자체 베어러 토큰을 받아들이며, Venice로 요청을 전달하고, 스트리밍 응답을 지원하며, 유용한 OpenTelemetry 스팬과 메트릭을 방출하는 게이트웨이를 갖게 됩니다.
전체 코드 구현이 궁금하다면 GitHub 저장소를 확인하세요.
사전 요구사항
- Rust 1.92+
- Docker 및 Docker Compose
- Venice API 키
curl- Rust 웹 서비스에 대한 기본적인 이해
무엇을 만들 것인가
레퍼런스 구현은 몇 가지 명확한 부분으로 구성된 작은 Rust 서비스입니다:
클라이언트가 OpenAI 호환 요청을 게이트웨이로 보냅니다. 게이트웨이는 호출자를 인증하고, 속도 제한을 확인한 뒤, Venice로 요청을 전달하고, 그 과정에서 텔레메트리를 기록합니다.
게이트웨이의 일부로, 우리는 이 서비스가 API 자체에 대해 최소한의 표면적을 유지하면서도 수평 확장 가능하도록 만들 것입니다. 여기에는 몇 가지 이유가 있는데, 그중 가장 큰 이유는 처리량이 매우 높다면 거의 확실히 레플리카를 사용하게 될 것이라는 점입니다(즉, 동일한 서비스를 1개 이상 띄우는 것). 이는 아직 그렇게 하지 않았다면 원본 서비스와 레플리카를 로드 밸런서 뒤에 두어야 함을 의미하며, 그래야 하나의 컨테이너나 서비스가 다운되어도 전체 서비스가 중단되지 않습니다.
또한 API 키 생성을 어떤 형태로든 우리가 소유한다고 가정하겠습니다. 다만 게이트웨이 서비스가 단독으로 키를 발급해서는 안 됩니다. 이는 로컬에서 사용할 때 시드 데이터를 채우는 Postgres 테이블로 표현될 것입니다. 프로덕션에서는 일반적으로 인증 서비스가 이를 처리합니다. LLM 게이트웨이를 사용하는 모든 사용자에 대해 업스트림에서 API 키를 생성하는 것도 가능하지만, 실무에서는 일반적으로 권장되지 않습니다. 이 책임을 업스트림 서비스에 넘기게 되면 여러분이 평소 가지고 있던 제어권도 함께 넘기게 되며, 결과적으로 속도 제한이나 지출 상한 같은 것들을 완전히 강제할 수 없게 됩니다.
소스 트리는 의도적으로 작게 유지됩니다:
Rust 서비스 생성하기
새로운 Rust 바이너리 프로젝트로 시작합니다:Cargo.toml에 필요한 의존성을 추가합니다 - 코드 스니펫에 설명이 포함되어 있습니다:
설정 불러오기
게이트웨이는 모든 것을 환경 변수에서 읽습니다. 이런 인프라 코드의 경우, 동일한 바이너리가 별도의 설정 파일 형식 없이도 로컬, Docker Compose 또는 호스팅 환경에서 실행될 수 있기 때문에 환경 변수가 좋은 기본값입니다. 또한 많은 제공자가 자체 컨테이너 런타임에서 환경 변수를 시크릿으로 저장할 수 있도록 허용합니다. 이는dotenv(또는 Rust에서는 원본 dotenv 크레이트가 대부분 deprecated 되었으므로 dotenvy) 같은 것을 사용하는 것보다 훨씬 안전한 경우가 많습니다.
src/config.rs를 생성합니다:
- 데이터베이스 URL
- Venice API 키
VENICE_BASE_URL은 https://api.venice.ai/api/v1을 가리키고, CAPTURE_GENAI_CONTENT는 기본값이 false이므로 의도적으로 활성화하지 않는 한 프롬프트 내용이 로깅되지 않습니다.
두 번째 기본값이 더 중요합니다. 게이트웨이는 자신을 통해 흐르는 모든 프롬프트와 응답을 볼 수 있지만, 관측성이 자동으로 콘텐츠 캡처가 되어서는 안 됩니다. 대부분의 프로덕션 시스템에서는 토큰 수, 지연 시간, 모델 이름, 상태 코드, 청구 메타데이터만으로도 운영에 충분합니다. 일반적으로 프로덕션에서 프롬프트와 대화를 로깅하는 것은 개인정보 문제뿐만 아니라 스토리지 문제도 될 수 있습니다. 이를 추가하는 것은 매우 높은 카디널리티(즉, 데이터셋 내 데이터의 고유성)를 가진 스팬과 트레이스를 생성하는 것을 의미합니다. 이는 관측성 데이터 검색 비용을 크게 늘릴 수 있으며, 데이터를 검색할 때 성능을 저하시킬 수도 있습니다.
데이터베이스 스키마 만들기
다음으로migrations/0001_api_keys.sql을 생성합니다.
각 게이트웨이 API 키는 처음 12자만 조회용 prefix로 저장하고, 전체 키의 SHA-256 해시를 함께 저장할 것입니다. 이렇게 하면 원시 자격 증명을 저장하지 않고도 게이트웨이가 후보 행을 빠르게 찾을 수 있습니다.
prefix는 비밀이 아닙니다. 인덱싱을 위해 존재합니다. 호출자가 전체 키를 제시했음을 증명하는 것은 해시입니다. 이는 많은 API 키 시스템에서 사용하는 기본 형태와 같습니다. 원시 키를 한 번 보여주고, 되돌릴 수 없는 표현으로 저장하며, 조회와 지원 워크플로우를 위해 짧은 prefix를 유지합니다.
- API 키는 절대 평문으로 저장되지 않습니다.
- 속도 제한 설정은 양수여야 합니다.
- 폐기된 키는 활성 상태로 남을 수 없습니다.
- 속도 제한 윈도우는 키, 시작 시간, 윈도우 길이로 고유하게 식별됩니다.
Venice 클라이언트 만들기
다음으로src/venice.rs를 만들겠습니다. 클라이언트가 알아야 할 것은 업스트림 chat completions URL, Venice API 키, 그리고 일시적인 실패에 대해 몇 번 재시도할지뿐입니다.
이 래퍼를 작게 유지하는 것은 의도적입니다. 게이트웨이가 Venice의 전체 API를 재구현해서는 안 됩니다. 기본적으로 게이트웨이의 역할은 서버 측 자격 증명을 첨부하고, 타임아웃을 적용하며, 재시도해도 안전한 요청을 재시도하고, 라우터가 전달할 수 있는 형태로 업스트림 응답을 반환하는 것입니다.
EventSource를 만듭니다:
serde_json::Value라는 점에 주목하세요. 이는 의도적인 호환성 선택입니다. 모든 가능한 chat-completion 필드를 Rust에서 모델링하면, 업스트림 API에 유용한 옵션이 추가될 때마다 게이트웨이를 업데이트해야 합니다. 다른 곳에서 필요한 것만 파싱함으로써, 게이트웨이 릴리스 없이도 새로운 Venice 파라미터가 통과될 수 있도록 합니다.
애플리케이션 상태 공유하기
src/state.rs를 생성합니다:
PgPool은 이미 공유 풀 핸들이며, Arc<Config>도 설정을 저렴하게 유지합니다.
이것은 모든 핸들러에 세 가지 동일한 것에 대한 접근을 제공합니다: 불변 설정, 풀링된 데이터베이스 연결, 그리고 Venice 클라이언트. 이들을 하나의 AppState에 담아 두면 핸들러가 전역을 읽는 대신 Axum 상태를 통해 의존성을 받기 때문에 나중에 테스트하기도 더 쉬워집니다.
게이트웨이 API 키 인증하기
클라이언트는 다음과 같이 게이트웨이 키를 보냅니다:src/auth.rs를 생성하고 Axum 추출기를 구현합니다. 추출기는 보호된 핸들러가 인증된 키를 요구한다고 선언할 수 있도록 합니다:
- 베어러 토큰을 파싱합니다.
- 처음 12바이트를 키 prefix로 취합니다.
- 전체 후보 토큰을 SHA-256으로 해시합니다.
- prefix로 활성 키 행을 로드합니다.
- 저장된 해시와 후보 해시를 상수 시간에 비교합니다.
AuthenticatedApiKey를 받는 라우트는 함수 본문 내부에서 실수로 인증을 건너뛸 수 없습니다. Axum이 핸들러가 실행되기 전에 그 값을 구성해야 하기 때문입니다. 이렇게 하면 보호된 경로를 감사하기 쉬워집니다.
고정 윈도우 속도 제한 추가하기
src/rate_limit.rs를 생성합니다. 속도 제한기는 새 윈도우를 삽입하거나 기존 윈도우를 증가시키기 위해 하나의 SQL 문을 사용합니다:
WHERE api_key_rate_limit_windows.request_count < $3 절이 핵심입니다. 윈도우가 이미 가득 찼을 때 Postgres는 행을 업데이트하지 않고 RETURNING은 아무 행도 생성하지 않습니다. 핸들러는 이를 Retry-After 헤더가 포함된 429 Too Many Requests 응답으로 바꿀 수 있습니다.
고정 윈도우가 가장 정교한 속도 제한기는 아니지만, 설명하기 쉽고, 검사하기 쉬우며, 게이트웨이 튜토리얼에는 충분히 좋습니다. 트레이드오프는 트래픽이 윈도우 경계 주변에서 몰릴 수 있다는 점입니다. 대규모에서 더 부드러운 동작이 필요하다면 Redis로 뒷받침되는 토큰 버킷이나 슬라이딩 윈도우 제한기가 자연스러운 다음 단계입니다.
OpenAI 스타일 오류 반환하기
src/error.rs를 생성하고 애플리케이션 오류가 IntoResponse를 구현하도록 만듭니다:
라우터 만들기
이제src/router.rs에서 HTTP 라우트를 연결할 수 있습니다:
AuthenticatedApiKey를 요구하는 것으로 시작합니다. 인증이 실패하면 Axum은 핸들러 본문에 진입하지 않습니다:
model, messages, stream. JSON 본문의 나머지 모든 것은 Venice로 통과합니다. 이렇게 하면 나중에 사용하고 싶은 제공자 기능과 게이트웨이의 호환성이 유지됩니다.
핸들러는 또한 두 가지 응답 모드를 명시적으로 만듭니다. 비스트리밍 요청은 Venice가 전체 JSON 응답을 반환할 때까지 기다린 다음, 다운스트림으로 바이트를 보내기 전에 응답 메타데이터를 기록합니다. 스트리밍 요청은 비동기 스트림으로 뒷받침되는 text/event-stream 본문과 함께 즉시 반환됩니다. 이 분리는 비스트리밍 경로를 단순하게 유지하면서 스트리밍 경로가 통과하는 청크를 관찰할 수 있는 충분한 제어권을 갖도록 합니다.
스트리밍 응답 지원하기
스트리밍 chat completions는 server-sent events를 사용합니다. Venice는 SSE 데이터를 보내고, 게이트웨이는 그 데이터를 클라이언트에게 다시 중계합니다. 게이트웨이는 전체 스트림을 버퍼링하지 않아야 합니다. 그렇게 하면 스트리밍의 목적이 무의미해지기 때문입니다. 사용자는 최종 토큰까지의 시간이 아니라 첫 번째 토큰까지의 시간에 관심이 있습니다. 각 업스트림 이벤트가 도착할 때마다 전달함으로써, 클라이언트는 모델이 여전히 생성 중인 동안 부분 출력을 렌더링할 수 있습니다.src/sse.rs를 생성합니다:
data: ... 이벤트로 도착하고, 스트림은 data: [DONE]로 끝납니다.
스트림 관찰자는 또한 클라이언트가 보는 것을 바꾸지 않고 메타데이터를 수집할 수 있는 곳입니다. 각 청크는 SSE 형식으로 전달되지만, 게이트웨이는 여전히 청크가 통과할 때 응답 ID, finish reason, 토큰 사용량, 비용 필드 및 타이밍 정보를 감시할 수 있습니다.
GenAI 텔레메트리 기록하기
게이트웨이가 유용한 이유는 모든 요청이 한 곳을 통과하기 때문입니다. 그래서 모델, 지연 시간, 토큰 사용량, finish reason, 청구 비용 및 스트리밍 타이밍을 기록하기에 좋은 지점입니다.src/telemetry.rs를 생성하고 요청 파싱부터 시작합니다:
id에서 옵니다:
서버 시작하기
이제src/main.rs에서 모든 것을 연결합니다:
- 설정을 읽습니다.
- 텔레메트리를 초기화합니다.
- Postgres에 연결합니다.
- SQLx 마이그레이션을 실행합니다.
- 공유 앱 상태를 구축합니다.
- Axum 서버를 시작합니다.
docker compose up으로 전체 스택을 작동 상태로 만들 수 있기 때문에 이 튜토리얼에 편리합니다. 더 큰 프로덕션 배포에서는 새 게이트웨이 인스턴스가 시작되기 전에 스키마 변경이 검토되고 적용되도록 마이그레이션을 별도의 릴리스 단계로 실행하는 것을 선호할 수 있습니다.
로컬 게이트웨이 키 시드하기
로컬 개발용으로scripts/seed_api_key.sh를 생성합니다. 이 스크립트는 게이트웨이 API 키의 prefix와 SHA-256 해시를 저장하여 Postgres에 삽입합니다:
로컬에서 실행하기
로컬에서 실행하려면 Docker Compose를 사용하여 게이트웨이와 Postgres를 함께 시작합니다. 이렇게 하면 튜토리얼을 재현 가능하게 유지할 수 있습니다: 독자는 수동으로 구성된 데이터베이스가 필요하지 않으며, 게이트웨이는 컨테이너화된 배포에서 사용하는 것과 동일한DATABASE_URL 형태를 사용할 수 있습니다.
-d 플래그로 분리된 상태로 실행할 수도 있습니다(그리고 나중에 docker compose down으로 제거하세요).
다른 터미널에서 개발 게이트웨이 키를 시드하세요:
5432 포트에서 실행 중이라면, Compose Postgres 서비스의 호스트 포트 매핑을 제거하세요. 게이트웨이는 Docker의 내부 네트워크에서만 Postgres에 도달하면 됩니다.
기억해야 할 중요한 점은 Venice API 키가 게이트웨이 환경에만 속한다는 것입니다. 클라이언트 요청은 시드된 게이트웨이 키를 사용해야 합니다. 그 분리가 바로 모델 제공자 앞에 게이트웨이를 두는 전체 이유입니다.
게이트웨이 테스트하기
먼저 상태를 확인합니다:이 게이트웨이 확장하기
이 게이트웨이는 의도적으로 작지만, 견고한 기반을 제공합니다. 좋은 다음 단계는 다음과 같습니다:- subject별 예산과 월별 지출 한도를 추가하세요.
- 동일한 OpenAI 호환 API 뒤에서 여러 업스트림 제공자를 지원하세요.
- 프롬프트 로깅을 기본적으로 비활성화된 상태로 유지하면서 감사 로그를 위해 요청 메타데이터를 저장하세요.
- 게이트웨이 키를 생성, 폐기 및 로테이션하기 위한 관리자 API를 추가하세요.
- API 키별 모델 허용 목록을 추가하세요.
- 여러 게이트웨이 인스턴스에서 더 낮은 지연 시간의 속도 제한이 필요하다면 Redis나 다른 공유 저장소를 추가하세요.