/v1/chat/completions 엔드포인트를 제공하고, 자체 베어러 토큰을 받아들이며, Venice로 요청을 전달하고, 스트리밍 응답을 지원하며, 유용한 OpenTelemetry 스팬과 메트릭을 방출하는 게이트웨이를 갖게 됩니다.
전체 코드 구현이 궁금하다면 GitHub 저장소를 확인하세요.
사전 요구사항
- Rust 1.92+
- Docker 및 Docker Compose
- Venice API 키
curl- Rust 웹 서비스에 대한 기본적인 이해
무엇을 만들 것인가
레퍼런스 구현은 몇 가지 명확한 부분으로 구성된 작은 Rust 서비스입니다:| 구성 요소 | 하는 일 |
|---|---|
| Axum 라우터 | /healthz와 /v1/chat/completions를 제공 |
| 인증 추출기 | Postgres에 해시된 형태로 저장된 키와 게이트웨이 베어러 토큰을 대조하여 검증 |
| 속도 제한기 | Postgres에 저장된 고정 윈도우를 사용 |
| Venice 클라이언트 | 스트리밍 및 비스트리밍 chat completion 요청을 프록시 |
| 텔레메트리 | GenAI 스팬, 토큰 사용량, Venice 청구 비용, 지연 시간, 스트리밍 타이밍을 기록 |
| Docker Compose | 로컬에서 Postgres와 게이트웨이를 실행 |

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나 다른 공유 저장소를 추가하세요.