Skip to content

ModelGateway 아키텍처 ​

ModelGateway는 포털 소비자에게 모델 접근 단일 창구를 제공하는 독립 backend 서비스다. OpenAI 호환 프록시, 키 격리, 쿼터·비용 집계, 메타데이터 감사를 담당한다.

  • 범위·이름 확정: UiPlatform/docs/MODEL_GATEWAY_SCOPE.md (2026-09-13, UiPlatform/docs/DECISION_LOG.md)
  • 소비자 이전 계획: ProcessCenter/docs/DB_STRATEGY.md §8

책임 분리 ​

구성 요소소유 책임소유하지 않는 책임
ModelGatewayOpenAI 호환 프록시, 모델 카탈로그·라우팅, 키 관리, 쿼터·레이트리밋, 비용 집계, 감사(메타데이터)검색·색인·근거 응답, 파일 보관, UI·테마, 사용자·권한, 업무 규칙, 프롬프트 내용
ExploreGateway검색 조합·문서 색인·벡터 검색·근거 기반 응답모델 키·쿼터·비용
소비 포털AI 적용 결정(무엇을 요약/번역할지), 프롬프트 내용, 업무 데이터모델 접근 구현
FileStore / UI플랫폼 / PortalConsole파일 / 표현 규칙 / 사용자·권한모델 접근

엔드포인트 ​

메서드경로설명
GET/v1/health상태·모델·감사 집계. 업스트림 주소·키는 노출하지 않는다
GET/v1/models허용 모델 목록 + 목적별 기본 모델 매핑(purposeModels)
POST/v1/chat/completions본문을 그대로 전달, model 미지정 시 기본 모델 주입
POST/v1/embeddings동일

인증: Authorization: Bearer <MODEL_GATEWAY_SERVICE_TOKEN> — 소비자별 서비스 토큰(상수 시간 비교). 귀속 헤더: x-portal-id·x-purpose·x-actor·x-tenant-id·x-organization-id — 없으면 null(미분류)로 기록하며 추정하지 않는다. 응답 헤더: x-request-id·x-model·x-usage-total-tokens.

POST /v1/chat/completions의 stream: true는 1차 미지원이므로 400 INVALID_REQUEST로 명시 거부한다(업스트림 SSE를 JSON으로 파싱하다 빈 응답으로 깨지지 않는다).

감사·비용·쿼터 ​

감사 레코드는 requestId·portalId·purpose·actor·tenantId·organizationId·model·promptTokens·completionTokens·cacheReadTokens·costUsd·latencyMs·status·deploymentProfile·createdAt만 기록한다.

  • 요금(USD): 기록 시점의 실효 단가표(기본 시드 ⊕ 저장된 override)로 costUsd를 계산해 함께 남긴다(src/pricing.ts).

    • 단가·공휴일은 운영 데이터라 modelPricing 실행 설정(DB)으로 관리한다 — 콘솔에서 고치면 재배포 없이 다음 요청부터 적용된다. env(기본 시드)와 MODEL_GATEWAY_PRICING_HOLIDAYS는 초기값일 뿐이다.
    • 모델별 override는 지정한 필드만 덮는다 — "공휴일만 갱신"해도 단가·피크 창·요일은 그대로다.
    • 피크/오프피크를 반영한다 — 오프피크가 피크의 절반. 피크는 01:00–04:00·06:00–10:00 UTC, 월–금이고 공휴일은 제외한다. ModelPrice.source가 같은 모델끼리 공휴일 캘린더를 공유한다(출처별 — 한 요금제의 공휴일이 다른 공급자에 섞이지 않는다).
    • 캐시 히트 입력은 캐시 단가로 계산한다(업스트림이 prompt_cache_hit_tokens(DeepSeek) 또는 prompt_tokens_details.cached_tokens(OpenAI 계열)를 줄 때).
    • 단가표에 없는 모델·토큰 미상은 null로 남긴다(0으로 뭉개지 않는다 — "미상"과 "무료"를 구별한다).
  • 요청·응답 본문은 저장하지 않는다(문서 내용·개인정보 최소화). 업무 맥락이 필요하면 소비자가 자기 DB에 남긴다.

  • 메모리 감사 상한: records는 MODEL_GATEWAY_AUDIT_MAX_ENTRIES(기본 200)로 상한을 두어 무인증 대량 요청에도 무한히 자라지 않는다. /v1/health의 totals는 상한과 무관한 누적 카운터다.

  • 인증 실패는 영속 감사에 남기지 않는다: 401은 메모리 상한 레코드와 totals.unauthorized 카운터로만 관측한다. 무인증 요청이 JSONL·PostgreSQL을 무한히 키우는 앰플리피케이션을 막기 위함이다.

  • 저장소는 두 가지다: DATABASE_URL이 있으면 PostgreSQL(영속), 없으면 메모리(개발·테스트). 메모리 저장소는 MODEL_GATEWAY_AUDIT_LOG_PATH가 있으면 JSONL로도 남긴다.

  • DB TLS: DATABASE_SSL — disable(기본, 평문) · require(암호화, 자체 서명 허용) · verify-full(암호화 + CA 검증). 미설정·알 수 없는 값은 평문이다.

  • 기동 안정성: DB 연결·DDL 적용이 실패해도 서비스는 메모리 저장소로 폴백해 기동한다(경고 로그). 프록시 기능이 DB 장애로 멈추지 않는다.

  • 영속 테이블(부팅 시 idempotent DDL, 정본은 src/storage.pg.ts의 MODEL_GATEWAY_DDL):

    • model_gateway_audit — 요청별 감사·사용량 레코드(request_id 유니크, portal/purpose/created_at 인덱스)
    • model_gateway_quota_policies — (portal_id, purpose) 일별 토큰 한도. purpose IS NULL은 포털 전체 한도
  • 쿼터: (portalId, purpose) 일별 토큰 한도가 있으면 프록시 호출 전에 확정 사용량 + in-flight 예약으로 판정한다. 한도에 닿으면 429 + Retry-After: 60 으로 막고 업스트림을 호출하지 않는다. 정책이 없으면 제한하지 않는다.

    • 예약(reservation)은 남은 예산을 넘지 않게 잡고(min(추정치, 남은 예산)), 응답을 감사에 기록한 뒤 해제한다. 같은 한도를 동시 요청이 함께 통과하는 check-then-act 경쟁을 막는다.
    • 추정치는 본문의 max_tokens가 있으면 그 값을, 없으면 MODEL_GATEWAY_QUOTA_RESERVE_TOKENS(기본 1000)를 쓴다.
  • 비용 조회: GET /v1/usage?portalId=&purpose=&days=(1~30일, 기본 7일)로 요청 수·실패 수·토큰·금액(costUsd, USD)·평균 지연을 조회한다. 응답의 currency가 금액 단위다. 게이트웨이는 UI를 갖지 않고 이 읽기 전용 API만 제공한다. 화면(대시보드) 소유자는 PortalConsole(제어평면 콘솔)로 확정한다(2026-09-14).

배포 프로필 ​

프로필모델 경로
CLOUD외부 모델 API(키 필요) 또는 사내 LLM
ON_PREM사내 LLM·임베딩만(외부 차단 허용)
미설정(서비스 없음)소비자는 aiCompletionEnabled() false → 기능 SKIPPED(현행 동작 유지)

보안 원칙 ​

  • 업스트림 키(MODEL_GATEWAY_UPSTREAM_API_KEY)는 이 서비스의 환경변수에만 존재하고 응답·로그·감사에 남기지 않는다.
  • 소비자에게는 서비스 토큰만 배포하며, 브라우저에 어떤 키도 노출하지 않는다.
  • 서비스 토큰 회전: MODEL_GATEWAY_SERVICE_TOKENS(쉼표 구분)로 여러 토큰을 동시에 유효하게 두고 소비자를 순차 교체한다(무중단).
  • 토큰 미설정은 fail-closed: NODE_ENV가 development/test로 명시된 환경에서만 개발 기본 토큰(model-gateway-development-only)을 허용한다. 미설정·production에서 토큰이 없으면 기동하지 않는다(공개된 기본 토큰으로 열리지 않는다).
  • 운영 구간은 TLS 뒤에 두고, 감사 로그에는 본문이 들어가지 않는다.
  • 인증 실패 레이트리밋: 소켓 주소(프록시 뒤에서는 trust proxy 기준)별로 창당 실패 수를 제한한다(MODEL_GATEWAY_AUTH_FAILURE_LIMIT, 기본 30 / MODEL_GATEWAY_AUTH_FAILURE_WINDOW_MS, 기본 60초). 초과 시 429 + Retry-After 로 대량 토큰 추측을 차단한다.
  • 귀속 헤더 길이 강제: docs/openapi.json이 선언한 maxLength(x-portal-id 64 · x-purpose 120 · x-actor 200 · x-tenant-id/x-organization-id 32)를 초과하면 400 INVALID_REQUEST 로 거부한다.

모델 폴백 정책 ​

  • MODEL_GATEWAY_FALLBACK_URL은 CLOUD에서만 사용한다. 업스트림이 연결 실패(502)·타임아웃(504)일 때 1회 재시도하고, 성공하면 응답 헤더 x-upstream-fallback: true로 표시한다.
  • ON_PREM에서는 폴백 설정이 있어도 사용하지 않는다(외부 모델 반출 금지).

모델 라우팅·키 회전·분당 한도 ​

  • 목적별 기본 모델: MODEL_GATEWAY_PURPOSE_MODELS(예: process.version-summary=llama3,rag.embedding=bge-m3)로 x-purpose별 기본 모델을 지정한다. 요청 본문에 model이 없을 때(또는 예약값 "auto"일 때) 적용하고, 매핑이 없으면 MODEL_GATEWAY_DEFAULT_MODEL을 쓴다. 매핑은 /v1/models의 purposeModels로 노출한다. 목적 어휘와 소비자 계약은 **docs/PURPOSE_REGISTRY.md**가 정본이다.
  • 업스트림 키 회전: MODEL_GATEWAY_UPSTREAM_API_KEYS(쉼표 구분)로 키를 여러 개 두면 앞에서부터 시도하고, 업스트림이 401/403을 주면 다음 키로 넘어간다(무중단 교체). 키 값은 응답·로그·감사에 남기지 않으며 /v1/health는 개수(keyCount)만 노출한다.
  • 분당 토큰 한도: MODEL_GATEWAY_PER_MINUTE_TOKEN_LIMIT(0이면 비활성)으로 포털×목적 창당 토큰을 제한한다. 일별 쿼터와 동일한 예약(reserve)·정산(settle) 방식을 써서 동시 요청이 한도를 함께 넘지 못하게 하고, 초과 시 429 + Retry-After(창 종료까지 초)로 응답한다.

프로바이더·관리자 화면 ​

ModelGateway는 외부 모델(DeepSeek·OpenAI·Claude)과 사내 LLM을 프로바이더로 등록해 한 창구에서 라우팅한다.

  • 종류: openai-compatible(DeepSeek·OpenAI·사내 LLM) · anthropic(Claude 전용 프로토콜을 OpenAI 호환으로 변환)
  • 내장 프리셋: deepseek · openai · anthropic · onprem — 모델 ID는 화면에서 수정한다
  • 라우팅: 요청 model이 속한 프로바이더를 고르고, 모르면 기본 프로바이더 → 기본 모델 순으로 결정한다. x-purpose 매핑이 있으면 모델 미지정 시 우선 적용한다
  • 키: 프로바이더별로 여러 개 등록할 수 있고(인증 오류 시 다음 키로 회전), AES-256-GCM으로 암호화해 저장하며 응답에는 마스킹된 힌트만 노출한다
  • 설정 우선순위: 관리자 저장값(DB) > env > 내장 기본값. DB에 프로바이더가 없으면 MODEL_GATEWAY_UPSTREAM_URL(env)을 단일 프로바이더로 사용한다(기존 배포 호환)
  • 임베딩: OpenAI 호환 프로바이더만 지원한다(Claude에 요청하면 400)

관리자 API (/admin/api) ​

게이트웨이는 UI를 갖지 않는다(확정: UiPlatform/docs/MODEL_GATEWAY_SCOPE.md §8-7 — 화면 소유자는 PortalConsole). 운영 화면은 PortalConsole(제어평면 콘솔) 의 좌측 관리자 메뉴 → 모델 게이트웨이가 소유하고, 콘솔 서버가 서버 간 토큰으로 아래 API를 호출한다. 브라우저는 게이트웨이에 직접 붙지 않는다(쿠키·CORS·핸드오프 없음).

메서드경로설명
GET/state콘솔 화면 초기 상태(프로바이더+키 힌트 · 설정 · 프리셋 · 환경 요약)
PUT / DELETE/providers/:id프로바이더 등록 · 수정 · 삭제
POST/providers/:id/keys키 추가(암호화 저장)
PUT / DELETE/keys/:keyId키 사용 여부 · 삭제
POST/providers/:id/verify연결 테스트(업스트림 GET {baseUrl}/models)
GET / PUT/settings실행 설정 조회 · 저장
GET/usage최근 사용량 집계
  • 인증: Authorization: Bearer <MODEL_GATEWAY_ADMIN_TOKEN> — 소비자용 MODEL_GATEWAY_SERVICE_TOKENS와 분리해 관리 권한을 좁게 유지한다. MODEL_GATEWAY_ADMIN_TOKENS(쉼표 구분)로 무중단 회전이 가능하고, 미설정이면 관리 API는 401이다.
  • 연결 테스트는 업스트림 GET {baseUrl}/models로 주소·키를 확인하고, 성공하면 업스트림이 알려준 모델 ID를 outcome.models로 돌려준다(프리셋 기본값과 공급자 카탈로그가 다를 때 유용). 모델 ID는 공급자·계정별로 다르므로 콘솔에서 확인 후 저장한다.

테스트 ​

tests/gateway.test.ts — 인증(401), 헬스(비노출 확인), 모델 목록, 프록시 본문·헤더 전달, 허용 모델 검증, stream: true 거부·stream: false 전달, 업스트림 실패 전달, 감사 기록(본문 미포함), 설정·인증 헬퍼(토큰 fail-closed·SSL 모드). tests/contract.test.ts — OpenAPI 3.1 문서와 실제 서버의 경로·메서드·오류 스키마·귀속 헤더 일치. tests/resilience.test.ts — 다중 서비스 토큰 회전, CLOUD 한정 모델 폴백(x-upstream-fallback), ON_PREM 폴백 금지, 업스트림 키 회전(401 → 다음 키). tests/usage.test.ts — 사용량 집계·/v1/usage 조회, 일별 쿼터 429(Retry-After), 분당 토큰 한도 429, in-flight 예약으로 동시 요청 차단, 인증 실패 비영속화, PostgreSQL SSL 모드 매핑. tests/budget.test.ts — 토큰 예산 원장(예약·정산·창 초기화·Retry-After 상한). tests/providers.test.ts — 프리셋·라우팅·입력 검증, OpenAI↔Anthropic 어댑터(요청 변환·응답 정규화·임베딩 거부·연결 테스트·오류 메시지). tests/admin.test.ts — 관리자 API 토큰 인증(401·잘못된 토큰), 프로바이더/키 CRUD와 마스킹, 연결 테스트, 설정 저장 반영, Claude 임베딩 거부, 관리 화면·세션 경로 부재 확인(/admin·/admin/api/session 404), 설정 저장소 암호화.

종료(SIGTERM·SIGINT) 시 서버를 닫고 저장소 커넥션을 정리한다(graceful shutdown).

.github/workflows/verify.yml이 push(main)·PR마다 pnpm install --frozen-lockfile → pnpm run check → pnpm run test를 실행한다.