콘텐츠로 이동

설정 가이드

이 가이드는 Continuum Router 설정 방법을 설명합니다. 라우터는 다양한 배포 시나리오에 맞춰 명확한 우선순위 시스템과 함께 여러 설정 방법을 지원합니다.

설정 섹션:

  • 서버 & 백엔드: 서버 설정, 백엔드 프로바이더, 연결 옵션
  • 헬스 & 캐싱: 헬스 체크, 요청 설정, 재시도, 캐싱, 로깅
  • 보안 & 관리: API 키, 인증, WebUI, 관리 엔드포인트, ACP
  • 예산 (Budgets): 데이터 플레인의 장기 윈도 토큰 및 비용 쿼터
  • 고급 설정: 글로벌 프롬프트, 모델 메타데이터, 핫 리로드, 트레이싱, 로드 밸런싱, 속도 제한
  • 예제: 설정 예제와 Rust Builder API

설정 방법

Continuum Router는 네 가지 설정 방법을 지원합니다:

  1. 설정 파일 (YAML) - 프로덕션 환경에 권장
  2. 환경 변수 - 컨테이너화된 배포에 적합
  3. 명령줄 인수 - 테스트 및 오버라이드에 유용
  4. Rust Builder API - 라이브러리 사용을 위한 타입 안전 프로그래밍 방식 설정

설정 파일 검색

라우터는 다음 위치에서 설정 파일을 순서대로 검색합니다. 각 위치에서 .yaml 확장자를 먼저, 그다음 .toml을 시도합니다:

  1. --config 플래그로 지정된 경로
  2. ./config.yaml 또는 ./config.toml (현재 디렉토리)
  3. ~/.config/continuum-router/config.yaml 또는 config.toml (사용자 설정 디렉토리)
  4. /etc/continuum-router/config.yaml 또는 config.toml (시스템 설정 디렉토리)

설정 우선순위

설정은 다음 우선순위로 적용됩니다 (높은 것부터 낮은 순):

  1. 명령줄 인수 (최고 우선순위)
  2. 환경 변수
  3. 설정 파일
  4. 기본값 (최저 우선순위)

이렇게 하면 파일에 기본 설정을 두고 컨테이너에서는 환경 변수로 특정 설정을 오버라이드하며 명령줄 인수로 임시 조정을 할 수 있습니다.

설정 파일 형식

전체 설정 예제

# Continuum Router 설정
# 전체 주석이 달린 샘플은 continuum-router --generate-config로 생성하세요

# 서버 설정
server:
  # bind_address는 단일 문자열 또는 주소 배열을 허용합니다
  # TCP 형식: "host:port", Unix 소켓 형식: "unix:/path/to/socket"
  bind_address: "0.0.0.0:8080"          # 단일 주소
  # bind_address:                        # 또는 다중 주소:
  #   - "0.0.0.0:8080"                   #   모든 인터페이스의 TCP
  #   - "unix:/var/run/router.sock"      #   Unix 소켓 (Linux/macOS, Windows 10 1809+)
  # socket_mode: 0o660                   # 선택 사항: Unix 소켓 파일 권한
  workers: 4                             # 워커 스레드 수 (0 = 자동 감지)
  connection_pool_size: 100              # 백엔드당 최대 유휴 연결 수
  # max_concurrent_requests: 64          # 동시 처리 요청 상한 (재시작 필요)
  # max_stream_buffer_bytes: 4294967296  # 프로세스 전체 SSE/릴레이 버퍼 예산, 기본값 4 GiB (재시작 필요)
  # shutdown_drain_timeout: "20s"        # 계획된 종료 시 드레인 마감 시간 (재시작 필요)
  # explain_response_header: false       # API 응답에 x-continuum-route 헤더 포함 (재시작 필요)
  # 공유 아웃바운드 HTTP 클라이언트 튜닝. 선택 사항이며 각 필드의 기본값은
  # 아래와 같습니다. 클라이언트는 기동 시 한 번만 만들어지므로 모든 필드가
  # 재시작을 요구합니다.
  # http_client:
  #   pool_idle_timeout: "300s"          # 유휴 풀 연결 유지 시간
  #   pool_max_idle_per_host: 64         # 오리진당 유지할 유휴 연결 수
  #   tcp_keepalive: "30s"               # TCP keep-alive 프로브 간격
  #   http2_keep_alive_interval: "15s"   # HTTP/2 PING 간격
  #   http2_keep_alive_timeout: "5s"     # PING 무응답 허용 시간
  #   http2_keep_alive_while_idle: true  # 활성 스트림이 없는 연결에도 PING 유지

# 모델 메타데이터 설정 (선택 사항)
model_metadata_file: "model-metadata.yaml"  # 기본(0층) 메타데이터 파일 경로
model_metadata_dirs:                        # 추가 model-metadata.d 드롭인 디렉터리
  - "/etc/router/metadata"                  # 관례 위치 다음에, 적은 순서대로 적용

# 로드 밸런싱 전략: RoundRobin (기본값), WeightedRoundRobin,
# LeastLatency, Random, ConsistentHash, PrefixAwareHash, EngineLoad, LeastBusy,
# LeastCost
selection_strategy: RoundRobin

# 백엔드 설정
backends:
  # 내장 설정이 있는 네이티브 OpenAI API
  - name: "openai"
    type: openai                         # 네이티브 OpenAI 백엔드 사용
    api_key: "${CONTINUUM_OPENAI_API_KEY}"  # 환경 변수에서 로드
    org_id: "${CONTINUUM_OPENAI_ORG_ID}"    # 선택적 조직 ID
    weight: 3
    models:                              # 사용할 모델 지정
      - gpt-5.6-sol
      - gpt-5.6-terra
      - o3-mini
      - text-embedding-3-large
    retry_override:                      # 백엔드별 재시도 설정 (선택 사항)
      max_attempts: 5
      initial_delay: "200ms"
      max_delay: "30s"
      backoff_multiplier: 2.0
      jitter: true
      retryable_status_codes: [429, 502, 503, 504]
      retryable_errors: [ConnectionError, TimeoutError]
      timeout: "60s"

  # 사용자 정의 메타데이터가 있는 일반 OpenAI 호환 백엔드
  - name: "openai-compatible"
    url: "https://custom-llm.example.com"
    weight: 1
    models:
      - "gpt-5.6-sol"
      - "gpt-5.6-luna"
    model_configs:                       # 메타데이터가 포함된 향상된 모델 설정
      - id: "gpt-5.6-sol"
        aliases:                         # 이 메타데이터를 공유하는 대체 ID (선택 사항)
          - "gpt-5.6"
        metadata:
          display_name: "GPT-5.6 Sol"
          summary: "복잡한 전문 작업을 위한 프런티어 GPT-5.6 모델"
          capabilities: ["chat", "vision", "code", "reasoning", "tool"]
          knowledge_cutoff: "2026-02"
          pricing:                       # 1M 토큰당 USD
            input_tokens: 5.0
            output_tokens: 30.0
          limits:
            context_window: 1050000
            max_output: 128000

  # 자동 URL 감지가 있는 Ollama 로컬 서버
  - name: "local-ollama"
    type: ollama                         # 기본값 http://localhost:11434
    weight: 2
    models:
      - "llama2"
      - "mistral"
      - "codellama"

  # vLLM 서버
  - name: "vllm-server"
    type: vllm
    url: "http://localhost:8000"
    weight: 1
    # 지정하지 않으면 모델이 자동으로 검색됩니다
    # 네임스페이스 접두사가 있는 모델 (예: "custom/gpt-5.6-sol")은
    # 자동으로 기본 이름의 메타데이터와 일치합니다 (예: "gpt-5.6-sol")
    model_configs:
      - id: "qwen3.8-27b"
        metadata:
          artifact:                      # 이 백엔드가 서비스하는 가중치 (선택)
            digest: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
            revision: "4c2e9a1"
            source: "hf://Qwen/Qwen3.8-27B-FP8"
          draft_model:                   # 추측 디코딩 드래프트 (선택)
            name: "qwen3.8-0.6b"

  # Google Gemini API (네이티브 백엔드)
  - name: "gemini"
    type: gemini                           # 네이티브 Gemini 백엔드 사용
    api_key: "${CONTINUUM_GEMINI_API_KEY}" # 환경 변수에서 로드
    weight: 2
    models:
      - gemini-3.1-pro-preview
      - gemini-3-flash-preview
      - gemini-2.5-pro
      - gemini-2.5-flash

# 헬스 모니터링 설정
health_checks:
  interval: "30s"                        # 백엔드 상태 확인 주기
  timeout: "5s"                          # 헬스 체크 요청 타임아웃
  unhealthy_threshold: 3                 # 비정상으로 표시되기 전 실패 횟수
  healthy_threshold: 2                   # 정상으로 표시되기 전 성공 횟수
  endpoint: "/health"                    # 헬스 체크에 사용되는 엔드포인트
  warmup_check_interval: "1s"            # 백엔드 워밍업(HTTP 503) 중 가속 체크 간격
  max_warmup_duration: "300s"            # 가속 워밍업 모드 최대 지속 시간
  block_startup: true                    # 프리워밍과 첫 헬스 체크가 끝날 때까지 리스너 바인딩을 대기
  prewarm_timeout: "10s"                 # 기동 시 프리워밍 프로브 요청 하나의 제한 시간 (변경하려면 재시작)

# 요청 처리 및 타임아웃 설정
timeouts:
  connection: "10s"                      # TCP 연결 설정 타임아웃
  request:
    standard:                            # 비스트리밍 요청
      first_byte: "30s"                  # 폐기 예정이며 동작하지 않음. 실제 예산은 `total`
      total: "180s"                      # 전체 요청 타임아웃 (3분)
    streaming:                           # 스트리밍 (SSE) 요청
      first_byte: "120s"                 # 첫 SSE 청크에 적용되는 실제 기한
      chunk_interval: "30s"              # 두 번째 청크부터의 최대 간격
      total: "600s"                      # 전체 스트리밍 타임아웃 (10분)
    image_generation:                    # 이미지 생성 요청 (DALL-E 등)
      first_byte: "60s"                  # standard.first_byte와 마찬가지로 폐기 예정이며 동작하지 않음
      total: "180s"                      # 전체 타임아웃 (기본 3분, 상한 600s)
    model_overrides:                     # 모델별 타임아웃 오버라이드
      gpt-5-latest:
        streaming:
          total: "1200s"                 # GPT-5용 20분
      gpt-5.6-sol:
        streaming:
          total: "900s"                  # GPT-4o용 15분
  # 이 섹션 전체의 검증 상한. 기동 시 고정되므로 핫 리로드, Admin API,
  # 설정 동기화는 이 값을 바꾸는 후보 설정을 거부합니다.
  # limits:
  #   max_standard_timeout: "900s"       # 기본값 540s, 절대 상한 1080s
  #   max_retry_timeout: "90s"           # 기본값 30s, 절대 상한 120s

# 전역 재시도 및 복원력 설정
retry:
  max_attempts: 3                        # 최대 재시도 횟수
  initial_delay: "100ms"                 # 재시도 간 초기 지연
  max_delay: "10s"                       # 재시도 간 최대 지연
  backoff_multiplier: 2.0                # 지수 백오프 배수
  jitter: true                           # 지연에 무작위 지터 추가
  retryable_status_codes: [429, 502, 503, 504]
  retryable_errors: [ConnectionError, TimeoutError]
  timeout: "30s"                        # 전체 재시도 예산
  prefer_untried_backends: false        # 이 요청이 아직 연결하지 않은 백엔드로 재시도

# 로깅 설정
logging:
  level: "info"                         # 로그 레벨: trace, debug, info, warn, error
  format: "json"                        # 로그 형식: json, pretty

# Files API 설정
files:
  enabled: true                         # Files API 엔드포인트 활성화/비활성화
  max_file_size: 536870912              # 최대 파일 크기 (바이트, 기본: 512MB)
  storage_path: "./data/files"          # 업로드된 파일 저장 경로 (~지원)
  retention_days: 0                     # 시작 시 보존 기간 초과 파일 정리 (일, 0 = 영구 보존)
  metadata_storage: persistent          # 메타데이터 백엔드: "memory" 또는 "persistent" (기본)
  cleanup_orphans_on_startup: false     # 시작 시 고아 파일 자동 정리

  # 인증 및 권한
  auth:
    method: api_key                     # "none" 또는 "api_key" (기본)
    required_scope: files               # 접근에 필요한 API 키 스코프
    enforce_ownership: true             # 사용자가 자신의 파일만 접근 가능
    admin_can_access_all: true          # admin 스코프는 모든 파일 접근 가능

  # OpenAI 백엔드 파일 처리 전략
  openai:
    strategy: base64                    # "base64" (기본) 또는 "upload"
    fallback_to_base64: true            # 업로드 실패 시 base64로 폴백
    cache_capacity: 1000                # 최대 캐시 파일 ID 매핑 수
    cache_ttl_secs: 86400               # 캐시 TTL (초, 기본: 24시간)

  # 파일 작업 엔드포인트별 속도 제한
  rate_limiting:
    enabled: false                      # Files API 속도 제한 활성화
    uploads_per_minute: 10              # 분당 최대 파일 업로드 수
    downloads_per_minute: 100           # 분당 최대 파일 다운로드 수
    operations_per_minute: 1000         # 분당 최대 목록/조회/삭제 작업 수

# 분산 추적 설정
tracing:
  enabled: true                         # 분산 추적 활성화/비활성화
  w3c_trace_context: true               # W3C Trace Context 지원 (traceparent 헤더)
  headers:
    trace_id: "X-Trace-ID"              # 추적 ID 헤더 이름
    request_id: "X-Request-ID"          # 요청 ID 헤더 이름
    correlation_id: "X-Correlation-ID"  # 상관 ID 헤더 이름

# 서킷 브레이커 설정
circuit_breaker:
  enabled: true                         # 프록시 데이터 경로 상태 머신/Admin 제어 활성화
  failure_threshold: 5                  # 회로 개방까지의 연속 실패 횟수
  failure_rate_threshold: 0.5           # 회로 개방 실패율 (0.0-1.0)
  minimum_requests: 10                  # 실패율 평가 전 최소 요청 수
  timeout: "60s"                        # 복구 시도(반개방)까지의 시간
  half_open_max_requests: 3             # 반개방 상태에서 허용되는 시험 요청 수
  half_open_success_threshold: 2        # 회로를 닫는 데 필요한 성공 횟수

# 일반 LLM 프록시 경로가 이 상태를 기록하고 조회합니다. 열린 서킷은 선택에서
# 제외되며, 상태 확인·재시도·폴백과 함께 추론 라우팅을 보호합니다.

# 속도 제한 설정
rate_limiting:
  enabled: true                         # 속도 제한 활성화
  storage: memory                       # "memory" 또는 "redis"
  limits:
    per_client:
      requests_per_second: 10
      burst_capacity: 20
    global:
      requests_per_second: 1000
      burst_capacity: 2000

# Admin API 설정
admin:
  max_backend_name_length: 64             # 백엔드 이름 최대 길이 (1-256, 기본값: 64)
  auth:
    method: bearer                         # 인증 방법: none, bearer, basic, ip_whitelist, api_key
    bearer_token: "${ADMIN_TOKEN}"         # Admin 인증 토큰
  stats:
    enabled: true                          # 통계 수집 활성화/비활성화
    retention_window: 24h                  # 윈도우 쿼리용 링 버퍼 보존 기간
    token_tracking: true                   # 응답 본문에서 토큰 사용량 파싱
    persistence:
      enabled: true                        # 재시작 시 통계 영속 활성화
      path: ./data/stats.json              # 스냅샷 파일 경로
      snapshot_interval: 5m                # 주기적 스냅샷 간격
      max_age: 7d                          # 시작 시 이보다 오래된 스냅샷은 폐기
    # GET /admin/usage/export용 레코드 단위 사용량 내보내기 (기본값: 꺼짐).
    # persistence가 켜져 있으면 레코드 하나가 스냅샷에 약 300바이트를 더하므로
    # (1,000,000개면 약 300 MB) max_records는 감사 기간에 맞춰 정하세요.
    usage_export:
      enabled: false                       # stats.enabled도 필요; 끄면 레코드를 비움
      retention: 7d                        # 해석할 수 없거나 0이면 로드 오류
      max_records: 1000000                 # 1,000에서 10,000,000; 가장 오래된 것부터 제거

# 메트릭 및 모니터링 설정
metrics:
  enabled: true                         # Prometheus 메트릭 수집 활성화
  path: "/metrics"                      # 메트릭 엔드포인트 경로
  port: 9090                            # 메트릭 전용 리스너 (0이면 끔), 변경하려면 재시작
  # bind_address: "0.0.0.0"             # 그 호스트, 기본값: 첫 번째 TCP server.bind_address 호스트
  auth:
    method: bearer
    bearer_token: "${METRICS_TOKEN}"    # auth를 생략하면 엔드포인트가 401을 반환

# 읽기 전용 NVIDIA Dynamo 워커 탐색 (빌드 기능 `dynamo`, 공식 릴리스
# 바이너리에 포함). 라우터는 Dynamo가 등록한 워커를 etcd에서, 한
# 네임스페이스의 DynamoWorkerMetadata 리소스와 EndpointSlice에서, 또는 그
# operator 라벨이 붙은 Pod에서 관찰만 하며, 쓰기·삭제·lease를 전혀 하지
# 않습니다. 재시작이 필요한 필드는 없습니다. `load.*`, `shim.*`, `kv.*`는
# 즉시 반영되고, 나머지(`source`·`endpoints`·`tls`·`auth`·모든 `kubernetes.*`
# 필드)는 watcher를 교체해 다시 접속하고 전체 재동기화합니다
# (`kubernetes.model`과 `kubernetes.models`는 `source: kubernetes`에서
# 무시되며 고쳐도 아무 변화가 없습니다). `event_bus`는 클러스터 내부 이벤트 브로커
# 리스너를 자체 수퍼바이저로 실행합니다(아래 "Dynamo 이벤트 브로커 리스너" 참고).
# architecture/dynamo-integration.md 참고.
dynamo:
  enabled: false                        # 마스터 스위치; `dynamo` 빌드 기능 없이는 비활성
  discovery:
    source: etcd                        # `etcd`(기본값), `kubernetes`(DynamoWorkerMetadata + EndpointSlice; etcd처럼 부하·용량 제공), 또는 `kubernetes_pods`(Pod만; 부하·용량 정보 없음: 라운드로빈 디스패치)
    etcd:
      endpoints: ["http://etcd:2379"]   # http(s) URL 또는 host:port; 즉시 반영
      namespace: "dynamo"               # 워커의 DYN_NAMESPACE와 일치해야 함
      # namespaces: ["prod", "canary"]  # 또는 여러 그래프(최대 16개), 각각 자기 `shim.pools` 항목에 묶임; 하나를 더하거나 빼도 다른 그래프의 워커는 유지
      watch_reconnect_backoff:
        initial: "500ms"                # watch 끊김 후 첫 재연결 지연
        max: "30s"                      # 지연이 2배씩 늘어나는 상한
    # kubernetes:                       # `source: kubernetes` 또는 `kubernetes_pods`일 때 필수; 블록이 있으면 항상 검증
    #   namespace: "dynamo"             # 플릿의 Kubernetes 네임스페이스 (dynamoworkermetadatas + endpointslices, 또는 pods에 get/list/watch)
    #   dynamo_namespace: "dynamo"      # 워커의 DYN_NAMESPACE (nvidia.com/dynamo-namespace 라벨 값이자 레코드의 네임스페이스)
    #   dynamo_namespaces: ["prod", "canary"]  # 또는 이 Kubernetes 네임스페이스 안의 여러 Dynamo 네임스페이스; `dynamo_namespace`와 배타적; `models` 항목은 각자 `dynamo_namespace`를 지정
    #   model: "Qwen/Qwen3-32B"         # 서빙 모델 이름; `kubernetes_pods`는 이것 또는 `models`가 필요(Pod에는 카드가 없음), `kubernetes`는 둘 다 무시. 또는 이것 대신:
    #   models:                         # nvidia.com/dynamo-component마다 한 항목; 목록에 없는 컴포넌트는 건너뜀
    #     - {component: "VllmWorker-decodeheavy", model: "solar-pro4"}
    #     - {component: "VllmWorker-prefillheavy", model: "solar-pro4-longcontext"}
    #   watch_reconnect_backoff: {initial: "500ms", max: "30s"}
  load:
    max_staleness: "3s"                 # 워커의 마지막 kv_metrics 샘플을 신선하다고 볼 창 (100ms-5m)
    transport: direct                   # direct | broker: kv_metrics·kv-events 구독이 퍼블리셔에 닿는 방식
  shim:
    backends: []                        # `dynamo.frontend --router-mode direct` 복제본(backends[].name); disaggregated_serving.enabled 필요
    # pools:                            # 네임스페이스가 여럿이면 `backends` 대신 네임스페이스마다 풀 하나; 고정된 요청은 자기 네임스페이스의 frontend에만 닿음
    #   - {namespace: "prod", backends: [dynamo-frontend-prod]}
    #   - {namespace: "canary", backends: [dynamo-frontend-canary]}
    model_map: {}                       # 후보 매칭용 클라이언트 모델 id -> Dynamo display_name (요청 본문은 그대로 전달됨)
  kv:                                   # KV 오버랩 인지 prefill 선택(빌드 기능 `dynamo-kv`); 모든 필드 즉시 반영
    enabled: false                      # kv-events 구독, 블록 인덱스 유지, 플릿의 아티팩트로 토큰화, 프리픽스 오버랩 크레딧 적용
    # artifact_cache_dir: /var/cache/continuum-router/dynamo-kv-artifacts  # 기본값: <사용자 캐시 디렉터리>/continuum-router/dynamo-kv-artifacts; blake3 검증
    allow_http_artifacts: true          # 카드의 http(s):// 아티팩트(워커 자체 메타데이터 엔드포인트) 가져오기 허용
    allow_hf_artifacts: false           # 카드의 hf:// 아티팩트를 Hub에서 가져오기(공개 인터넷 egress; HF_ENDPOINT / HF_TOKEN 존중)
    pinned_artifacts: []                # 라우터가 로드해도 되는 파일의 blake3 체크섬; 핀 전에는 아무것도 로드·컴파일·렌더링하지 않음(비어 있으면 validate가 경고)
    # pinned_artifacts: ["blake3:<64 hex>", ...]  # 검토한 tokenizer.json / tokenizer_config.json / chat_template.jinja에 `b3sum`을 돌린 값
    overlap_score_credit: 1.0           # 프리픽스 오버랩 블록당 크레딧 블록(유한, >= 0)
    prefill_load_scale: 1.0             # 조정된 prefill 항의 배율(유한, >= 0)
    router_temperature: 0.0             # 0 = 최저 비용 선택; > 0이면 비용에 대한 softmax 샘플링(<= 100)
    send_token_data: false              # 렌더링한 토큰 id를 nvext.token_data로 전송(라우터의 토크나이저 버전을 플릿과 결합)
    conditional_disaggregation:         # 유효 ISL이 짧으면 원격 prefill 생략; Dynamo 1.5.x direct 모드 frontend는 거부
      enabled: false
      policy: isl_bounding              # isl_bounding | prefill_load | isl_or_load
      eff_isl_threshold: 2048           # 유효 ISL이 이 토큰 수 미만이어야 함
      eff_isl_ratio_threshold: 0.7      # 유효/원래 ISL 비율이 이 값 미만이어야 함(0-1)
      # prefill_busy_threshold: 0.8     # prefill_load / isl_or_load: max_num_batched_tokens의 이 비율 초과 시 busy
      # decode_busy_threshold: 0.9      # busy한(또는 신호 없는) decode 워커로의 우회 거부
  event_bus:                            # 클러스터 내부 이벤트 브로커 리스너(빌드 기능 `dynamo-event-bus`); `enabled`와 독립적
    enabled: false                      # continuum-event-broker가 접속하는 TLS 리스너 시작; 끄면 모든 세션을 드레인
    listen: "0.0.0.0:7443"              # enabled일 때 필수; 전용 포트이며 server.bind_address 포트와 겹칠 수 없음
    tls:                                # enabled일 때 필수; TLS 1.3, ALPN h2, 평문 거부
      cert: "/etc/continuum-router/event-bus/tls.crt"
      key: "/etc/continuum-router/event-bus/tls.key"
      # client_ca: "/etc/continuum-router/event-bus/client-ca.crt"  # mTLS: DNS SAN이 브로커 id인 인증서가 그 브로커로 인증
    brokers:
      - id: "broker-a.dynamo"           # 소문자 [a-z0-9.-], 63바이트 이하; 브로커의 hello.client_id와 같아야 함
        token_hash: "sha256:0000000000000000000000000000000000000000000000000000000000000001"  # 자리표시자: printf %s "$TOKEN" | sha256sum; 토큰 자체는 금지
        namespaces: ["dynamo"]          # 이 브로커가 선언할 수 있는 Dynamo 네임스페이스; 비우면 모두 허용(validate가 경고)
      # - id: "kv-listener-a"           # 푸시 모드의 continuum-kv-listener
      #   role: kv_listener             # broker(기본값) | kv_listener
      #   token_hash: "sha256:0000000000000000000000000000000000000000000000000000000000000003"  # 플레이스홀더
      #   backends: ["vllm-1"]          # 이 리스너가 알릴 수 있는 kv_cache_index event_sources backend_name 값
    max_sessions: 64                    # 라우터 전체의 welcome된 세션 수(1-1024)
    max_streams_per_session: 1024       # 세션당 구독 수(1-1024)
    max_session_buffer_bytes: 67108864  # 세션당 바이트 예산; 이 기본값에서 스트림·방향당 16 KiB
    heartbeat_interval: "10s"           # 1s-30s
    idle_timeout: "30s"                 # heartbeat_interval의 2배 이상, 300s 이하

# Continuum Hub control-plane 에이전트 (`control-plane` 빌드 기능 필요)
control_plane:
  enabled: false                        # 마스터 스위치
  hub_url: "https://hub.example.com"    # enabled일 때 필수
  enrollment_token: "${CONTINUUM_HUB_ENROLLMENT_TOKEN}"
  state_file: "./data/control-plane/state.json"
  heartbeat_interval_secs: 30           # 하트비트 주기(초, 기본값: 30)
  usage_push_interval_secs: 10          # 사용량 푸시 주기(초, 기본값: 10)
  usage_batch_max_records: 500          # 사용량 푸시 배치당 최대 레코드 수(기본값: 500)
  request_timeout_secs: 10              # 아웃바운드 Hub 요청별 타임아웃(초, 기본값: 10)
  config_sync:
    enabled: false
    poll_interval_secs: 5
    state_file: "./data/control-plane/config-sync.json"
    backends:
      mode: disabled                    # disabled | overlay | authoritative
      state_file: "./data/control-plane/backend-sync.json"
      tasks: true                       # 아웃바운드 후보 프로브 및 마스킹 내보내기 작업 채널
      max_concurrent_tasks: 2           # 동시 실행할 Hub 백엔드 작업 수(1-8, 기본값: 2)

# 클라이언트용 System One 결정 엔드포인트: POST /v1/decisions (별칭 /v1/systemone).
# `targets`에 있는 이름은 해당 System One 서버로 그대로 전달하고 그 외 이름은
# 같은 이름의 채팅 모델에서 에뮬레이션합니다(강제 `emit_decision` 도구 호출 1회,
# 응답에 `emulated: true` 표시). 즉시 반영됩니다. api.md의 Decisions 절 참고.
decisions:
  enabled: true                         # false면 404 decisions_disabled 응답
  targets:
    - name: jev                         # 클라이언트가 보내는 `model`; 폴백 체인에서도 사용 가능
      protocol: typesafe                # generic (기본값) | typesafe | foundry | llama_cpp
      auth: auto                        # auto (기본값) | bearer | api_key | entra
      endpoint: "https://api.typesafe.ai/v1/systemone"
      api_key_env: TYPESAFE_API_KEY     # 또는 `backend: <자체 호스팅 backends[] 이름>`
      model: jev-1.14.0                 # 업스트림 모델 또는 Foundry 배포 이름
      typesafe_images: false            # TypeSafe JSON assets 및 Box 질문 옵트인
      typesafe_long_context: false      # TypeSafe 262k 롱 컨텍스트 헤더 옵트인
      max_questions: 64                 # 1-256 (기본값: 64)
      max_state_bytes: 16384            # 256-1048576 (기본값: 16384)
      max_media_bytes: 4194304          # 디코딩된 미디어 바이트 1-16777216 (기본값: 4 MiB)
      timeout_ms: 1500                  # 100-60000 (기본값: 1500)
      max_concurrent: 32                # 1-1024 (기본값: 32)
  emulation:
    enabled: true                       # false면 타깃이 아닌 이름은 404
    max_state_bytes: 16384              # 256-1048576
    max_tokens: 1024                    # 16-32768

# control-plane 사용량 레코드의 요청별 품질 판정 (control-plane 빌드와
# control_plane.enabled 필요). /v1/chat/completions 응답이 버퍼링되었든
# 스트리밍되었든 모든 가드레일 단계를 통과하고 나면 System One 결정 모델이
# 판정하고 확률은 해석된 판정 모델 id와 함께 basis point로 사용량 레코드에
# 실립니다. 응답, 레코드의 회계, 푸시는 판정을 기다리지 않습니다.
# 즉시 반영됩니다.
usage:
  quality_judgment:
    target:                             # 공유 System One 타깃 블록
      endpoint: "http://laya.internal:8000/v1/systemone"
    sample_rate: 1.0                    # 판정할 요청 비율
    models: []                          # 제공된 모델 id; 비어 있으면 모든 모델
    include_tool_results: false         # true면 현재 턴의 도구 결과를 보냄 (`grounded`용)
    max_request_bytes: 512              # 앞뒤를 남기는 상한, Laya 다국어 기준
    max_reply_bytes: 1536
    max_tool_results_bytes: 1024
    max_in_flight: 32                   # 라우터별 동시 판정 수; 초과분은 버림
  # 스트림 종료 후 엔진 /tokenize 보정(기본값 꺼짐): 라우터가 직접 센 델타 수로
  # 기록되는 스트림을 클라이언트 스트림이 끝난 뒤 서빙 엔진의 토크나이저 경로
  # (vllm, sglang, llamacpp, mlxcel, continuumrouter)로 다시 세어 최대
  # timeout_ms 뒤에 `engine_tokenize`로 기록합니다. 스트림이 끝날 때마다 읽습니다.
  tokenize_correction:
    enabled: true                       # 기본값 false
    timeout_ms: 2000                    # 100-30000
    max_concurrent: 4                   # 라우터별 1-256; 초과분은 대기 없이 버림
  # control-plane 에이전트가 만드는 모든 사용량 배치를 SHA-256 체인 해시와 함께
  # 추가 전용 로컬 파일에 기록. 파일 검증은
  # `python3 -I scripts/verify-usage-ledger.py <file>`. 다음 푸시 주기에 반영됩니다.
  ledger_export:
    path: "/var/lib/continuum-router/usage-ledger.jsonl"  # 설정하지 않으면 내보내지 않음
    max_bytes: 1073741824               # 1 GiB를 넘으면 회전 (최소 1 MiB)
    rotate: true                        # false면 max_bytes에서 내보내기를 멈춤
    max_files: 30                       # 회전 파일 보관 개수(최소 1). 생략하면 모두 보관
  # 백엔드별, 윈도별로 원장 출력 토큰을 엔진 생성 카운터와 비교.
  # engine_stats.enabled: true가 필요하며 즉시 리로드됩니다.
  reconciliation:
    window: "5m"                        # 10s-24h, engine_stats.interval의 두 배 이상 권장
    max_drift: 0.05                     # (0, 1]
    history: 24                         # 백엔드별로 보관할 닫힌 윈도 수(1-1000)

사용량 원장 내보내기

usage.ledger_export는 control-plane 에이전트가 만드는 모든 사용량 배치를 체인 해시와 함께 배치당 JSON 한 줄씩, 추가 전용이고 소유자만 접근할 수 있는(0600) 파일에 기록합니다. 라우터는 이 파일을 잘라내지 않고, 회전된 파일은 max_files를 설정한 경우에만 지웁니다. control-plane 빌드와 control_plane.enabled: true가 필요합니다. 아래 표의 규칙을 어기는 값은 로드 오류이므로 시작, 핫 리로드, config validate가 모두 거부합니다. 줄 형식, 회전 앵커, 감사 절차는 변조 증거를 참고하세요.

필드 유형 기본값 설명
path string 설정 안 함 내보내기 파일. 설정하지 않으면 아무것도 내보내지 않습니다. 비어 있으면 안 되고 제어 문자를 포함할 수 없으며 control_plane.state_file이나 그 이름을 딴 파일일 수 없습니다(ledger_export_path_conflicts_with_state_file). 상위 디렉터리는 자동으로 만듭니다.
max_bytes integer 1073741824 이 크기를 넘으면 파일 이름을 <path>.<sent_at_ms>로 바꾸고 새 파일을 앵커 줄로 시작합니다. 최소 1048576.
rotate bool true false면 max_bytes를 넘는 줄을 거부하고 usage_ledger_export_failures_total{target="export"}로 셉니다.
max_files integer 설정 안 함 보관할 회전 파일의 최대 개수이며 현재 파일은 세지 않습니다. 회전할 때마다, 그리고 writer가 시작하거나 리로드로 교체될 때 한도를 넘는 가장 오래된 파일을 지우는데, 순서는 파일 이름의 <sent_at_ms>와 -<n>으로 정합니다. 이름이 정확히 <path>.<sent_at_ms> 또는 <path>.<sent_at_ms>-<n>인 일반 파일만 세며, 심볼릭 링크와 디렉터리, 다른 이름의 파일은 지우지 않습니다. 설정하지 않으면 회전 파일을 모두 보관합니다. 최소 1(ledger_export_max_files_zero).

버퍼링 요청의 내부 스트리밍

proxy.internal_streaming(기본값 false)은 생성 도중 취소된 버퍼링 클라이언트 요청을 계량합니다. 활성화하면 조건을 만족하는 버퍼링(stream이 없거나 false) /v1/chat/completions 및 /v1/completions 요청을 stream: true와 stream_options: {include_usage: true}(vllm 백엔드에는 continuous_usage_stats: true도 추가)로 백엔드에 전송하고, 스트림을 읽어 조립한 일반 비스트리밍 JSON 본문으로 클라이언트에 응답합니다. 클라이언트가 생성 도중 연결을 끊으면 라우터가 그때까지 수신한 스트림 델타를 집계해 outcome: client_cancelled 레코드 하나를 기록합니다. 이 기능을 끄면 생성 도중 취소된 요청은 사용량 기록을 남기지 않습니다.

proxy:
  internal_streaming: true   # 즉시 재로드

backends:
  - name: vllm-a
    type: vllm
    url: http://vllm-a:8000
    internal_streaming: false   # 이 백엔드는 버퍼링 유지
필드 타입 기본값 설명
proxy.internal_streaming bool false 조건을 만족하는 버퍼링 chat 및 completions 요청을 백엔드에서 스트리밍으로 받습니다.
backends[].internal_streaming bool 미설정 백엔드별 재정의입니다. 미설정이면 proxy.internal_streaming을 따릅니다. anthropic, gemini, bedrock, azure, unix: URL에서 true는 로드 오류입니다.

다음 조건을 모두 만족할 때만 내부 스트리밍합니다.

  • 클라이언트 /v1/chat/completions 또는 /v1/completions 경로로 들어온 요청입니다(재시도, 폴백 홉, 중복 제거, 웹 검색 턴, 품질 홉, 분리형 서빙과 Dynamo shim 디스패치 포함). /v1/decisions 에뮬레이션, Anthropic Messages와 Responses 인그레스, 임베딩, 리랭크, 이미지, 파일, 합성 프로브, 라우터가 시작하는 모든 호출은 버퍼링으로 남습니다.
  • 백엔드 타입이 HTTP(S) 위의 vllm, sglang, tensorrt-llm, llamacpp, mlxcel, ollama, lmstudio, openai, generic, continuumrouter입니다.
  • n이 없거나 1이고 logprobs가 없거나 false 또는 null이며 top_logprobs가 없고 response_format.type이 없거나 text 또는 json_object입니다.
  • 요청에 functions, function_call, audio, web_search_options가 없고 modalities에 audio가 없습니다. 조립한 본문은 이들이 요청하는 출력(function_call, audio, annotations)을 다시 만들지 않기 때문입니다.

클라이언트는 여전히 버퍼링 본문을 받습니다. content, reasoning_content, refusal, tool_calls(도구 호출만 생성되면 content: null), 선택지별 finish_reason, 엔진이 보낸 경우 엔진의 usage가 담깁니다. 가드레일, 응답 캐시, 품질 검사도 그 본문을 봅니다. 사용량 레코드는 다음과 같습니다.

  • 완료된 스트림: 제공된 응답 행을 기록하며, 엔진 사용량 블록이 없으면 라우터가 집계한 토큰 수로 usage_missing을 기록합니다.
  • 클라이언트 취소: 라우터가 집계한 토큰 수, stream: false, http_status: 200으로 client_cancelled 레코드 하나를 남깁니다. 응답 헤더를 받은 뒤 첫 데이터 이벤트 전에 취소된 경우에도 client_cancelled, 출력 0, 추정 입력 토큰으로 시도를 기록합니다.
  • 출력 시작 후 실패: 부분 생성된 토큰 수로 upstream_error(또는 timeout_mid_stream)를 기록하고 502(또는 504)로 응답합니다. 이는 최종 실패로 처리되며, 이미 일부 출력이 생성된 요청은 재시도나 폴백 홉(분리형 서빙의 통합 폴백 포함)으로 다시 보내지 않습니다.
  • 출력 시작 전 실패: 이벤트가 전혀 없거나, 오류 이벤트가 도착했거나, role만 담긴 청크 뒤에 전송 오류·타임아웃이 발생했거나, [DONE]과 finish_reason 없이 본문이 끝난 경우에는 일반 버퍼링 읽기 실패처럼 재시도하고 폴백합니다. role 외의 델타 필드(텍스트, 추론, 도구 호출, 거부)는 모두 출력으로 간주하므로 그중 하나라도 생성된 스트림은 재전송하지 않습니다. 출력이 시작된 뒤 [DONE]과 finish_reason 없이 끝난 본문은 스트림 도중 끊긴 실패로 기록하며, finish_reason이 있으면 완료된 응답으로 처리합니다.
  • 비스트리밍 업스트림 응답 및 크기 한도: 스트림 요청에 단일 application/json 본문으로 응답한 백엔드는 응답을 수정 없이 그대로 전달합니다(백엔드당 1회 로그 기록). 내부 스트리밍 응답은 누적 SSE 전송 데이터 16 MiB로 제한되며 각 읽기를 파싱·조립하기 전에 크기를 확인합니다. 한도를 초과하면 재시도나 폴백 없이 502를 반환하고 한도 도달 전까지 관측된 사용량을 upstream_error로 기록합니다.

chat 스트림의 사용량

라우터는 /v1/chat/completions 스트림을 계량하려고 모든 백엔드에 사용량을 요청합니다(stream_options.include_usage: true). Dynamo 프런트엔드처럼 요청하지 않아도 사용량을 보내는 엔진도 있습니다. 클라이언트가 보낸 include_usage가 스트림에 실리는 내용을 결정하며, 모델과 상관없이 동일하게 적용됩니다.

  • include_usage 미설정 또는 false: 클라이언트는 usage를 전혀 받지 않습니다. 모든 청크에서 usage 필드를 제거하고(null 포함), 마지막 사용량 전용 청크(choices: []와 usage)는 통째로 뺍니다.
  • include_usage: true: 클라이언트는 OpenAI 스트림이 보내는 형태대로 마지막 사용량 전용 청크에서만 usage를 받습니다. usage: null을 담은 청크는 그대로 전달하고, 선택지와 null이 아닌 usage를 함께 담은 청크에서는 usage를 제거합니다(청크마다 사용량을 보내는 엔진의 running usage). 이런 청크 뒤로 사용량 전용 청크 없이 [DONE]에 이르거나 끝난 스트림에는 라우터가 마지막으로 제거한 usage를 담은 사용량 전용 청크를 써 넣으므로, 마지막 본문 청크에만 사용량을 보내는 엔진도 사용량을 한 번 전달합니다.

그 밖의 이벤트(본문 청크, [DONE], keep-alive, event: error)는 그대로 전달합니다. 계량, 응답 캐시, 가드레일 게이트는 필터보다 앞에서 스트림을 읽으므로 사용량 기록은 어느 경우에도 같습니다.

  • 적용 범위: /v1/chat/completions 스트림에만 적용합니다. 비스트리밍 응답, /v1/completions, Anthropic Messages 인그레스, Responses API에는 영향이 없습니다.
  • 지원 종료 필드: proxy.honor_include_usage_models는 이 동작이 기본값이 된 뒤로 지원이 종료되어 무시됩니다. 필드는 여전히 파싱되지만, 모델을 나열해도 아무 변화가 없습니다.

응답별 비용 헤더

proxy.response_cost_headers(기본값 false)를 켜면 백엔드에서 받은 응답에 비용과 토큰 정보를 붙입니다. 비스트리밍 응답에는 응답 헤더로, 스트리밍 응답에는 HTTP trailer로 전달합니다.

proxy:
  response_cost_headers: true   # 즉시 재로드
헤더 값
x-continuum-cost-microusd 응답 비용을 반올림한 정수 마이크로 USD(1,000,000 = $1). 모델의 pricing으로 계산하며, 프롬프트가 임계값을 넘으면 long_context 구간 요금을, 캐시된 프롬프트 토큰에는 cached_input_discount를 적용
x-continuum-tokens-in 응답의 usage.prompt_tokens
x-continuum-tokens-out 응답의 usage.completion_tokens
x-continuum-tokens-cached 백엔드가 보고한 캐시 프롬프트 토큰 수. 보고하지 않으면 생략
x-continuum-latency-ms 백엔드 왕복 지연 시간(밀리초)
  • 대상 경로: 버퍼링된 /v1/chat/completions(proxy.internal_streaming 요청 포함), /v1/completions, 그리고 chat-completions 브리지로 처리한 /v1/responses 요청입니다.
  • 사용량 기록과 같은 값: 토큰 수는 통계와 사용량 원장이 기록하는 usage 블록에서 그대로 가져옵니다. 품질 폴백 홉이 응답을 대체하면 헤더도 그 홉의 모델 요금, 토큰 수, 지연 시간을 따릅니다.
  • 데이터가 없을 때: pricing 메타데이터가 없는 모델은 비용 헤더만 빠지고, usage 블록이 없는 응답에는 지연 시간 헤더만 붙습니다.
  • 붙지 않는 경우: 응답 캐시 적중은 백엔드 왕복이 없으므로 제외합니다. 임베딩, 리랭크, 이미지, 오디오, Anthropic Messages 인그레스도 대상이 아닙니다.
  • usage 블록의 비용: 이 설정과 상관없이, pricing 메타데이터가 있는 모델의 버퍼링된 /v1/chat/completions 응답에는 비용 헤더와 같은 요금으로 계산한 usage.cost와 usage.cost_details.upstream_inference_prompt_cost / upstream_inference_completions_cost가 USD 단위로 담깁니다(이슈 #2404). 항목별로 정수 마이크로 USD로 반올림하므로, usage.cost가 헤더와 1마이크로 USD 다를 수 있습니다. pricing이 없는 모델에는 비용 필드가 없고, 응답 캐시 적중은 응답을 저장할 때 기록한 비용을 그대로 돌려줍니다.

비용은 공급자 청구서가 아니라 라우터의 모델 카탈로그로 계산하고, 이 설정은 모든 API 키에 똑같이 적용됩니다. 모든 클라이언트에 카탈로그 가격을 공개해도 될 때만 켜세요. 브라우저 클라이언트는 cors.expose_headers에 이 헤더들을 넣어야 읽을 수 있습니다.

스트리밍 응답

스트리밍 응답은 사용량이 확정되기 전에 헤더가 먼저 나가므로, 같은 다섯 필드를 헤더 대신 HTTP trailer로 전달합니다. 별도의 스트림 설정은 없으며 위 토글을 그대로 사용합니다.

  • 예고: 응답 헤더에 Trailer: x-continuum-cost-microusd, x-continuum-tokens-in, x-continuum-tokens-out, x-continuum-tokens-cached, x-continuum-latency-ms가 담깁니다.
  • 값: 본문 마지막 프레임 뒤의 trailer 구간에 값이 담깁니다. 요금 계산, 생략 규칙, 그리고 스트림을 만든 홉의 사용량은 헤더와 같습니다. x-continuum-latency-ms는 요청 시작부터 사용량 확정 시점까지 측정합니다.
  • 협상: HTTP/1.1 클라이언트는 TE: trailers로 필드를 요청하며, 이 헤더가 있을 때만 hyper가 trailer를 인코딩합니다. 헤더가 없으면 trailer 필드 없이 본문만 전달되지만, 필드는 선택적 메타데이터이므로 본문은 완전하게 도착합니다. HTTP/2는 협상 없이 trailer를 전달합니다.
  • 적용 범위: 사용량을 계량하는 스트리밍 파이프라인입니다. /v1/chat/completions, /v1/completions, /v1/responses와 그 브리지가 여기에 해당합니다. Anthropic Messages 인그레스와 버퍼링된 SSE 응답 같은 다른 Server-Sent Events 표면은 Trailer 헤더도 trailer 필드도 보내지 않습니다. 사용량이 확정되기 전에 끝난 스트림(도중에 끊긴 클라이언트, 업스트림 실패, 가드레일 차단)은 trailer 프레임을 보내지 않습니다.
  • 브라우저: cors.expose_headers는 응답 헤더를 브라우저 자바스크립트에 노출하지만, 브라우저는 trailer를 읽을 수 없습니다. 따라서 브라우저 클라이언트는 스트림 요청별 비용을 사용량 원장이나 버퍼링 응답에서 확인해야 합니다.

헤지 요청

proxy.hedge_delay_ms(기본값 미설정, 0~60000)를 설정하면 느린 디스패치를 두 번째 백엔드와 경합시킵니다. 버퍼링된 /v1/chat/completions 또는 /v1/completions 디스패치가 첫 백엔드를 고른 뒤 이 시간(밀리초) 안에 응답하지 않으면, 라우터는 같은 모델을 서빙하는 두 번째 백엔드에 같은 요청을 보내고, 먼저 도착한 응답을 채택하며, 나머지 요청을 취소합니다. 0이면 두 요청을 동시에 보냅니다.

proxy:
  hedge_delay_ms: 500   # 즉시 재로드, 미설정이면 비활성
  • 경합하지 않는 경우: 클라이언트 스트림(stream: true인 레거시 /v1/completions 요청 포함), 폴백 체인 홉, 임베딩, rerank, 이미지 생성은 경합하지 않습니다. replay: disabled 백엔드는 취소된 시도도 이미 본문을 받았으므로, 첫 백엔드일 때 경합하지 않고 두 번째 대상으로도 고르지 않습니다.
  • 두 번째 대상: 첫 선택과 같은 모델, 가시성, 허용 목록 파이프라인으로 고르되 첫 백엔드는 제외합니다. 디스패치할 때 회로 차단기와 레이트 리밋 승인을 확인하며, 거부되면 첫 시도만 계속 진행합니다.
  • 회계: 두 시도 모두 각 백엔드의 회로 차단기와 디스패치 승인에 반영됩니다. 취소된 시도는 outcome: hedge_cancelled인 별도 사용량 행으로 기록되고, 요청 자체의 행은 승자의 것입니다. x-backend 선호를 보낸 요청은 x-served-backend로 승자를 확인할 수 있습니다.

모델 변형 접미사

proxy.variant_suffixes.enabled(기본값 false)를 켜면 /v1/chat/completions에서 OpenRouter 방식의 모델 변형 접미사를 사용할 수 있습니다(이슈 #2405). 정확히 해석되지 않는 모델 이름이 알려진 접미사로 끝나면 마지막 :에서 분리해 베이스 ID를 해석합니다. 요청한 이름 전체를 먼저 해석하므로, Ollama의 name:tag처럼 콜론을 포함해 실제로 서빙되는 이름은 그대로 동작하며, 다른 인그레스의 동작도 바뀌지 않습니다.

proxy:
  variant_suffixes:
    enabled: true   # 기본값 false, 즉시 재로드
접미사 동작
:nitro 정상이고 승인 가능한 후보 중 최신 generation_throughput_tps가 가장 높은 후보로 좁힌 뒤, 설정된 선택 전략이 그중에서 고릅니다.
:floor least_cost와 같은 가격 및 비용 추정(동률이면 출력 단가)으로 추정 비용이 가장 낮은 후보로 좁힌 뒤, 설정된 선택 전략이 그중에서 고릅니다.
:thinking 클라이언트가 지정하지 않았다면, 메타데이터가 thinking 지원(thinking 항목 또는 비어 있지 않은 reasoning_efforts)을 선언한 모델에 reasoning_effort: "high"를 설정합니다. 클라이언트가 지정한 값이 우선합니다.
:online 라우터가 관리하는 web_search 루프를 실행합니다.
  • 조용한 대체 없음: :nitro와 :floor는 실패하지 않습니다. 최신 처리량이나 가격 데이터가 있는 후보가 없으면 일반 선택이 요청을 처리하고, 응답에 변형 단어를 담은 x-continuum-model-variant와 함께 x-continuum-model-variant-applied: false가 붙습니다.
  • 거부: 메타데이터가 thinking 지원을 선언하지 않은 모델의 :thinking, 사용할 수 있는 web_search 실행기가 없는 경우(비활성, 상용 백엔드, 설정 없음)의 :online, 스트리밍 요청의 :online은 접미사를 명시한 404를 반환하며 error.type은 "invalid_request_error", error.code는 "unsupported_model_variant"입니다. 알 수 없는 접미사 단어는 지금처럼 알 수 없는 모델로 처리됩니다.
  • 식별자와 집계: 키별 allowed_models와 모델별 레이트 리밋은 베이스 ID를 기준으로 하며, 백엔드에도 베이스 ID가 전달됩니다. 요청한 이름(베이스와 접미사)은 응답의 model에 그대로 반영되고, 폴백 체인 홉은 변형을 물려받지 않습니다.
  • 관측: 접미사가 붙은 이름으로 처리한 요청은 continuum_model_variant_requests_total{variant, applied}에 집계됩니다. 순위를 매길 데이터가 없었거나 백엔드별 web_search 재정의가 주입을 거부하면 applied는 false입니다.

Middle-out 메시지 변환

proxy.middle_out는 컨텍스트 윈도우를 넘는 /v1/chat/completions 요청을 백엔드의 context-length 오류 대신 피팅합니다. 운영자 스위치와 클라이언트 opt-in이 모두 필요하며, 둘 중 하나라도 없으면 요청은 변경 없이 전달됩니다.

proxy:
  middle_out:
    enabled: false                 # 즉시 재로드, 기본값은 꺼짐
    default_output_reserve: 1024   # 0~1000000, 요청에 출력 한도가 없을 때 사용
  • 클라이언트 opt-in: 요청 본문 최상위의 "transforms": ["middle-out"] 배열이며, ASCII 기준으로 대소문자를 구분하지 않습니다. transforms 값이 배열이 아니면 무시합니다.
  • 피팅 규칙: 공용 입력 추정치가 모델의 컨텍스트 윈도우에서 출력 예산을 뺀 값을 넘어야 합니다. 출력 예산은 max_completion_tokens, 없으면 max_tokens, 둘 다 없으면 default_output_reserve이며, 윈도우는 컨텍스트 윈도우 입장 검사와 같은 제한 스냅샷에서 가져옵니다.
  • 제거 대상: 대화 중간의 메시지를 오래된 것부터 통째로 제거합니다. 선두 system/developer 연속 구간과 현재 턴(마지막 user 메시지와 그 뒤의 모든 메시지)은 제거하지 않으며, assistant의 tool call은 그 tool 결과와 함께 제거합니다.
  • 응답 신호: 피팅된 응답에는 버퍼링과 스트리밍 모두 x-continuum-middle-out: dropped=<n>;estimated_input_tokens=<n>;target_input_tokens=<n>;context_window=<n> 헤더가 붙고, 사용한 middle-out 항목은 디스패치 전에 transforms에서 제거됩니다.
  • 피팅하지 않는 경우: 보존 메시지만으로 목표를 넘으면 페이로드를 그대로 전달하고 요청은 평소 경로를 탑니다. 설정된 경우 컨텍스트 윈도우 폴백을 타고, 그렇지 않으면 백엔드 오류를 반환합니다. 피팅된 요청은 입장 검사를 통과하므로 이 폴백 홉을 추가로 타지 않습니다.

사용량 대조

usage.reconciliation은 백엔드별·윈도별로 사용량 원장이 기록한 출력 토큰 수와 engine_stats가 스크레이프하는 엔진 누적 생성 카운터의 증가분을 비교하여, 그 결과를 GET /admin/usage/reconciliation과 usage_reconciliation_* 메트릭으로 제공합니다.

  • 지원 백엔드 타입: 엔진 카운터는 vllm, sglang, llamacpp, mlxcel 백엔드만 보고하며(llamacpp는 --metrics 플래그 필요), 다른 백엔드 타입은 모두 no_engine_data를 보고하고 config validate가 이를 경고합니다. 다른 라우터나 클라이언트와 공유하는 엔진처럼 대조에서 제외할 백엔드에는 reconciliation: false를 설정하세요.
  • 빌드 및 검증: 이 블록은 항상 컴파일되며 control-plane 빌드가 필요하지 않습니다. 아래 표의 범위를 벗어난 값은 설정 로드 오류로 처리됩니다. 윈도 규칙은 엔진 카운터와의 대조를 참고하세요.
필드 유형 기본값 설명
enabled bool true 블록을 추가하면 대조기가 켜집니다. false로 하거나 블록을 지우면 상태를 비웁니다.
window duration 5m 윈도 길이, 10s부터 24h(reconciliation_window_out_of_range). 대조하는 각 백엔드의 실제 engine_stats.interval(backends[].engine_stats.interval 재정의, 없으면 전역 값)의 두 배 이상으로 두세요. 그보다 짧으면 config validate가 경고합니다.
max_drift number 0.05 within_tolerance로 볼 최대 abs(drift_ratio), (0, 1] 범위의 유한한 값(reconciliation_max_drift_out_of_range).
history integer 24 백엔드별로 보관할 닫힌 윈도 수, 1-1000(reconciliation_history_out_of_range).

사용량 블록이 없는 응답 기록

backends[].record_usage_missing(기본값 false; 별칭 record_missing_usage, 즉시 리로드)을 켜면 /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/rerank, /v1/responses, /anthropic/v1/messages 경로의 비스트리밍 2xx 응답에 사용 가능한 usage 블록이 없을 때(예: 자체 호스팅 임베딩·리랭크 서버나 버퍼링 응답에서 usage를 생략하는 OpenAI 호환 프록시) 0토큰 사용량 레코드를 기록합니다.

backends:
  - name: "self-hosted-embeddings"
    type: vllm
    url: "http://embeddings:8000"
    record_usage_missing: true   # 기본값 false; 즉시 리로드
필드 유형 기본값 설명
backends[].record_usage_missing bool false 이 백엔드가 사용 가능한 usage 블록 없이 비스트리밍 추론 경로에서 2xx로 응답할 때 outcome: usage_missing, token_source: none, estimated: false, 0토큰(prompt: 0, completion: 0, cache_read: 0)으로 사용량 레코드 하나를 기록합니다. usage_records_total{outcome="usage_missing",token_source="none"}에 집계됩니다.

false(기본값)이면 usage 블록이 없는 비스트리밍 2xx 응답은 Hub 사용량 행을 남기지 않습니다. true로 설정해도 유효한 usage 블록이 있거나 proxy.internal_streaming으로 이미 기록된 응답은 일반 사용량 레코드 하나만 남기며 4xx/5xx 및 전송 실패 동작은 바뀌지 않습니다. 생성된 행의 모든 토큰 수가 0이므로 가격 계산 비용, 키별 TPM 윈도, 월간 토큰 증분, 코스트센터 토큰 예산에는 영향을 주지 않습니다.

최소 설정

# 최소 설정 - 나머지 설정은 기본값 사용
server:
  bind_address: "0.0.0.0:8080"

backends:
  - name: "ollama"
    url: "http://localhost:11434"
  - name: "lm-studio"
    url: "http://localhost:1234"

health_checks:
  interval: "30s"
  timeout: "5s"
  unhealthy_threshold: 3
  healthy_threshold: 2
  endpoint: "/health"

logging:
  level: "info"
  format: "json"

Dynamo 이벤트 브로커 리스너

dynamo.event_bus는 클러스터 내부 이벤트 브로커 터널의 라우터 측을 실행합니다. 클러스터 내부의 continuum-event-broker가 아웃바운드로 접속하는 전용 TLS 리스너로, Kubernetes 클러스터 외부의 라우터가 직접 연결할 수 없는 워커 이벤트 퍼블리셔에 도달할 수 있게 합니다.

실제 동작에는 dynamo-event-bus Cargo 기능(dynamo 위에 구성되며 공식 릴리스 바이너리에 포함되지만 default/full에는 포함되지 않음)이 필요합니다. 설정 블록은 항상 컴파일·검증되며, 해당 기능 없이 빌드된 바이너리는 시작 시 브로커 리스너가 비활성 상태라고 로그를 남깁니다. dynamo.enabled와는 독립적으로 동작합니다. 프로토콜과 리스너 검증 규칙은 Dynamo 워커 탐색을, 실행 가능한 템플릿은 docs/shared/llm-skills/config-templates/dynamo-event-bus.yaml을 참고하세요.

필드 타입 기본값 규칙
enabled bool false 마스터 스위치
listen string 미설정 ip:port 소켓 주소(IPv6는 대괄호), 포트는 0이 아니어야 함; enabled일 때 필수; 어떤 TCP server.bind_address와도 겹칠 수 없음(같은 IP의 같은 포트, 또는 어느 한쪽이 0.0.0.0 / [::]인 같은 포트는 로드 오류)
tls.cert 경로 tls가 있으면 필수 PEM 인증서 체인, 리프 인증서가 먼저
tls.key 경로 tls가 있으면 필수 PEM 개인 키(PKCS#8, SEC1, PKCS#1)
tls.client_ca 경로 미설정 mTLS 브로커 신원을 켜는 PEM CA 번들: 이 CA로 이어지고 DNS SAN이 정확히 브로커 id인 클라이언트 인증서는 그 브로커로 인증됩니다. 미설정이면 클라이언트 인증서를 요청하지 않으며 모든 브로커에 token_hash가 필요합니다
tls object 미설정 enabled일 때 필수; 이벤트 버스는 평문으로 제공되지 않음
brokers array [] 최대 256개; enabled일 때 최소 1개
brokers[].id string 필수 소문자 [a-z0-9.-], 양 끝은 영문자나 숫자, 63바이트 이하, 고유; 브로커의 hello.client_id와 같아야 함
brokers[].token_hash string 미설정 bearer 토큰 SHA-256의 소문자 16진수 64자리 앞에 sha256:를 붙인 값(printf %s "$TOKEN" \| sha256sum), 토큰 자체는 금지; 고유; tls.client_ca가 설정된 경우에만 생략 가능
brokers[].role enum broker broker(클러스터 내부 continuum-event-broker) 또는 kv_listener(푸시 모드의 continuum-kv-listener). hello는 자격 증명의 항목이 설정된 역할에 대해서만 수락되며 broker이면 직렬화 출력에서 생략됩니다. id는 두 역할에 걸쳐 고유해야 합니다
brokers[].backends array of strings [] role: kv_listener 전용: 리스너가 알리고 명령받을 수 있는 kv_cache_index.event_sources[].backend_name 값. 리스너에는 필수이며 비어 있으면 안 되고 각 항목은 [A-Za-z0-9_-] 1~64바이트, 고유, 최대 256개이며 broker 항목에서는 로드 오류
brokers[].namespaces array of strings [] role: broker 전용(kv_listener 항목에서는 로드 오류). 브로커가 선언할 수 있는 Dynamo 네임스페이스(각 1~128바이트, /·공백·제어 문자·유니코드 줄 구분자·bidi 서식 문자 불가; 고유; 최대 64개). 비우면 모든 네임스페이스를 허용하며 config validate와 시작 로그가 경고
max_sessions integer 64 1~1024; 라우터 전체의 welcome된 세션 상한(초과 시 429 too_many_sessions). TCP 연결은 4 x max_sessions(최소 16)로 제한
max_streams_per_session integer 1024 1~1024; welcome.limits.max_streams로 알림
max_session_buffer_bytes integer 67108864 max_streams_per_session * 64 KiB 이상 1 GiB 이하; 스트림별 방향당 버퍼는 이 값을 4 * max_streams_per_session으로 나눈 뒤 16 KiB~1 MiB로 제한한 값
heartbeat_interval duration "10s" 1s~30s
idle_timeout duration "30s" heartbeat_interval의 2배 이상, 300s 이하

이 블록에는 재시작이 필요한 필드가 없습니다. 핫 리로드 시:

  • enabled는 리스너를 시작하거나, 모든 세션에 drain {reason: shutdown, deadline_ms: 2000}을 보내고 2초 뒤 종료 draining 오류로 끝낸 다음 리스너를 중지합니다.
  • listen은 새 주소로 리스너를 재시작합니다.
  • tls.*는 PEM 파일을 다시 읽어 새 연결에 사용하며 이미 맺어진 연결은 기존 것을 유지합니다. client_ca가 바뀌면 인증서로 인증된 모든 세션도 credential_revoked로 닫히고 이전 client_ca로 인증서를 검증한 연결은 그 인증서로 다시 인증할 수 없습니다(그 연결의 새 세션은 401 credential_revoked). 경로가 바뀔 때만 변경으로 인식하므로, 인증서·키·CA 파일의 내용을 같은 경로에서 바꿔치기해도 경로가 바뀌거나 라우터를 재시작하기 전까지는 반영되지 않습니다. CA를 폐기할 때 유의하세요.
  • 브로커를 제거하거나 token_hash를 바꾸면 그 브로커의 활성 세션이 credential_revoked로 닫히고 이후 제거된 자격 증명에는 401 unauthenticated 대신 401 credential_revoked로 응답합니다.
  • 브로커의 namespaces를 좁혀 활성 세션이 선언한 네임스페이스를 더 이상 포함하지 않으면 그 세션은 forbidden으로 닫힙니다. kv_listener 항목의 backends를 좁히거나 항목의 role을 바꾸면 더 이상 맞지 않는 활성 세션이 forbidden으로 닫힙니다.
  • 스트림·버퍼 한도가 바뀌면 기존 HTTP/2 윈도를 교체하도록 세션을 drain하고 리스너를 다시 시작합니다. max_sessions와 두 타이머는 새 세션부터 적용됩니다.

브로커 토큰 해시는 디버그 출력과 마스킹된 설정 내보내기에서 가려집니다. 마스킹된 설정을 다시 가져오면 브로커 순서가 바뀌어도 id를 기준으로 기존 해시를 복원합니다.

TLS 연결은 연결 상한 안에서 자리를 스스로 증명해야 합니다. TCP 수락 후 10초 안에 요청이 하나도 수락되지 않으면 연결을 닫고(continuum_dynamo_event_bus_connections_refused_total의 auth_timeout), 마지막으로 수락된 요청이 끝난 뒤 30초가 지나도 닫습니다. 거부된 요청은 이 타이머에 포함되지도, 타이머를 갱신하지도 않습니다. 두 기한은 고정값이며 설정할 수 없습니다.

시작에 실패한 리스너(TLS 파일 누락, 사용 중인 포트)는 event bus listener failed to start; retrying으로 로그를 남기고 성공하거나 섹션이 바뀔 때까지 30초마다 다시 시도합니다.

브로커를 통한 구독

dynamo.load.transport는 kv_metrics 부하 구독과 kv-events KV 구독이 ZMQ 퍼블리셔에 닿는 방식을 정합니다.

값 동작
direct (기본값) 라우터가 각 퍼블리셔의 tcp://host:port 엔드포인트에 직접 접속합니다. 라우터가 워커 Pod IP에 닿을 수 있어야 합니다.
broker 각 구독을, 허가된 네임스페이스가 퍼블리셔의 Dynamo 네임스페이스를 포함하는 활성 dynamo.event_bus 세션으로 전달합니다. 클러스터 내부 브로커가 퍼블리셔에 접속해 바이트를 중계하고 라우터는 그 위에서 같은 ZMTP 클라이언트, 디코더, 상한, 퍼블리셔 ID 검사를 실행합니다.

dynamo.load.transport를 broker로 설정하면 구독 수명 주기와 관측 지표는 다음과 같이 동작합니다.

  • 대기(pending) 상태 및 시작 지터: 어떤 활성 브로커 세션도 해당 네임스페이스를 담당하지 않으면 구독은 pending 상태로 머물며(연결을 시도하지 않으며, 해당 워커는 신선한 부하 샘플이 없는 워커로 취급됨), 담당 브로커 세션이 수락(welcome)되면 최대 500ms의 무작위 지연 후 구독을 시작합니다. 이를 통해 새로 연결된 브로커가 대기 중인 모든 구독을 한꺼번에 처리하는 현상을 방지합니다.
  • 랑데부 배치 및 페일오버: 여러 브로커 세션이 동일한 네임스페이스를 담당할 때는 퍼블리셔 ID와 브로커 ID의 안정적인 랑데부 해시 순위에 따라 각 구독을 브로커에 분산 배치하며, 재연결 시에도 동일한 브로커로 돌아갑니다. 브로커 세션이 종료되면 그 브로커가 담당하던 구독은 최소 백오프(최대 500ms, 지수 증가 없음) 후 다음 순위의 담당 브로커로 재연결하고, 남은 담당 브로커가 없으면 pending 상태가 됩니다. kv-events 랭크는 브로커 전환 중에도 커서를 유지하며, 60초 동안 연결되지 못한 kv-events 구독은 KV 인덱스에서 해당 랭크가 정리됩니다.
  • 빌드 요구 사항: broker 모드에는 dynamo-event-bus 빌드 기능과 활성화된 dynamo.event_bus 블록이 필요합니다. 해당 기능 없이 빌드된 바이너리는 기동 시 경고를 남기고 구독을 pending 상태로 유지하며 직접 연결로 폴백하지 않습니다.
  • 핫 리로드: 이 필드는 즉시 반영됩니다. 두 구독 수퍼바이저가 새 전송 방식으로 구독을 다시 시작하며, 워커 탐색·레지스트리·부하 저장소·KV 인덱스는 그대로 유지됩니다.
  • 관측 가능성 (GET /admin/dynamo/workers 및 메트릭): 관리 엔드포인트는 실행 중인 모든 구독을 subscriptions 아래에 family, publisher_id, namespace, component, state(pending, connecting, connected, backoff), transport, 그리고 전달 중인 broker ID와 함께 나열합니다(퍼블리셔 엔드포인트는 노출하지 않음). 구독 게이지에는 transport 레이블이 추가되고, continuum_dynamo_broker_subscriptions{family, broker}는 브로커별 연결된 구독 수를 집계합니다. broker 모드에서는 같은 엔드포인트에 broker_coverage(설정된 네임스페이스별 브로커 담당 여부, 담당 브로커 ID 목록, 패밀리별 대기 구독 수)가 추가되며, continuum_dynamo_broker_namespace_sessions{namespace}(0이면 미담당)와 continuum_dynamo_broker_pending_subscriptions{family, namespace} 메트릭도 동일한 정보를 보고합니다.

환경 변수

환경 변수는 다음 두 가지 방식으로 설정에 반영됩니다.

  1. 직접 오버라이드: 시작 시 설정 파일의 해당 값을 덮어쓰는 소수의 CONTINUUM_* 변수.
  2. 일반 ${VAR} 보간: 시크릿뿐 아니라 모든 문자열 값 YAML/TOML 필드가 파일 로드 시 보간됩니다. logging.level: "${LOG_LEVEL}", files.storage_path: "/data/${TENANT}", 타임아웃 문자열, URL, 경로가 모두 동작합니다.

    • ${VAR}: VAR의 값으로 확장됩니다.
    • ${VAR:-default}: VAR가 설정되어 있으면 그 값, 없으면 리터럴 default(빈 문자열도 가능)로 확장됩니다.
    • $$: 리터럴 $로 이스케이프됩니다. 따라서 $${VAR}는 리터럴 텍스트 ${VAR}가 됩니다.
    • 변수 이름은 [A-Za-z_][A-Za-z0-9_]*와 일치해야 하며 이름이 유효하지 않은 ${...}는 리터럴로 남습니다. 문자열 스칼라만 보간되므로, 숫자나 불리언이 필요한 자리의 ${VAR}는 타입 파싱에 실패합니다.

    시작 및 핫 리로드 시 기본값 없는 미설정 ${VAR}는 설정 경로를 명시하는 오류로 로드를 중단시킵니다. 반면 continuum-router config validate는 미설정 참조를 경고로 보고하고 플레이스홀더를 유지하므로, 프로덕션 시크릿 없이도 템플릿을 검증할 수 있습니다.

직접 오버라이드

변수 타입 설명
CONTINUUM_BIND_ADDRESS string server.bind_address 오버라이드
CONTINUUM_BACKEND_URLS string 쉼표로 구분된 백엔드 URL. backends 목록을 오버라이드
CONTINUUM_LOG_LEVEL string logging.level 오버라이드 (trace, debug, info, warn, error)
CONTINUUM_SELECTION_STRATEGY string selection_strategy 오버라이드. RoundRobin, WeightedRoundRobin, LeastLatency, Random, ConsistentHash, PrefixAwareHash, EngineLoad, LeastBusy, LeastCost를 허용합니다. 인식할 수 없는 값은 조용히 대체되지 않고 시작이 실패합니다. 우선순위: --selection-strategy > 이 변수 > 파일 값 > 기본값 RoundRobin
CONTINUUM_FILES_AUTH_METHOD string files.auth.method 오버라이드. none 또는 api_key를 허용합니다. 파일에 files: 섹션이 없어도 적용됩니다. 인식할 수 없는 값은 조용히 대체되지 않고 시작이 실패합니다
CONTINUUM_FILES_AUTH_SCOPE string files.auth.required_scope 오버라이드
CONTINUUM_FILES_ENFORCE_OWNERSHIP boolean files.auth.enforce_ownership 오버라이드. 정확히 true 또는 false(소문자)만 허용하며 다른 값은 시작이 실패합니다
CONTINUUM_FILES_ADMIN_ACCESS_ALL boolean files.auth.admin_can_access_all 오버라이드. CONTINUUM_FILES_ENFORCE_OWNERSHIP과 동일한 엄격한 true/false 문법을 사용합니다
RUST_LOG string 완전한 tracing 필터 지시문(예: continuum_router::proxy=debug,info)으로 logging.level과 CONTINUUM_LOG_LEVEL을 오버라이드

네이티브 백엔드 API 키

네이티브 백엔드는 설정에 api_key가 없을 때 다음 변수로 폴백합니다:

변수 사용처
CONTINUUM_OPENAI_API_KEY type: openai 백엔드
CONTINUUM_OPENAI_ORG_ID type: openai 백엔드 (선택적 조직 ID)
CONTINUUM_ANTHROPIC_API_KEY type: anthropic 백엔드
CONTINUUM_GEMINI_API_KEY type: gemini 백엔드

API 키 관리

변수 타입 기본값 설명
CONTINUUM_API_KEY string - 간단한 배포용 단일 API 키
CONTINUUM_API_KEY_SCOPES string "read,write" API 키의 쉼표로 구분된 스코프
CONTINUUM_API_KEY_USER_ID string "env-user" API 키와 연결된 사용자 ID
CONTINUUM_API_KEY_ORG_ID string "env-org" API 키와 연결된 조직 ID
CONTINUUM_DEV_MODE boolean false 개발용 API 키 활성화 (프로덕션에서 사용 금지). true 또는 1(대소문자 무시)만 키를 켜며, false, 0, 빈 값, 그 밖의 값은 키를 끈 상태로 둡니다. 설정하지 않으면 디버그 빌드는 키를 켜고 릴리스 빌드는 켜지 않습니다.

모델 메타데이터 내려받기 토큰

continuum-router metadata download는 아래 변수를 순서대로 확인해 비어 있지 않은 첫 값을 GitHub 토큰으로 씁니다. 전체 규칙은 모델 메타데이터 내려받기를 참조하세요.

변수 타입 설명
CONTINUUM_GITHUB_TOKEN string 권장. continuum-router 저장소 읽기 권한이 있는 GitHub 토큰
GITHUB_TOKEN string 대체. CI 러너가 이미 내보내는 이름
GH_TOKEN string 대체. GitHub CLI가 이미 내보내는 이름

저장소가 비공개인 동안에는 토큰이 필수이며 토큰은 raw.githubusercontent.com과 api.github.com에만 전송되고 로그나 출력에는 남지 않습니다.

환경 설정 예제

# 직접 오버라이드
export CONTINUUM_BIND_ADDRESS="0.0.0.0:9000"
export CONTINUUM_BACKEND_URLS="http://localhost:11434,http://localhost:1234"
export CONTINUUM_LOG_LEVEL="debug"

# config.yaml의 ${...} 치환에서 참조
export CONTINUUM_OPENAI_API_KEY="sk-..."
export ADMIN_TOKEN="my-admin-token"

# 라우터 시작
continuum-router

그 밖의 설정(헬스 체크, 타임아웃, 재시도, 캐싱, files.auth를 제외한 나머지 Files API 설정 등)은 모두 설정 파일로 지정합니다.

명령줄 인수

명령줄 인수는 최고 우선순위의 설정 방법으로 테스트와 임시 오버라이드에 유용합니다.

핵심 옵션

continuum-router --help
인수 타입 설명
--mode <MODE> enum 서버 모드: http (기본값) 또는 stdio (ACP JSON-RPC 2.0 전송)
-c, --config <FILE> path 설정 파일 경로
--generate-config flag YAML 샘플 설정을 생성하고 종료
--generate-example-config flag 예제 설정 문서(환경 변수, 라우팅, 검증 규칙, 우선순위)를 생성하고 종료
--generate-toml-config flag TOML 형식 샘플 설정을 생성하고 종료
--model-metadata <FILE> path 모델 메타데이터 YAML 파일 경로 (설정 파일보다 우선)

백엔드 설정

인수 타입 설명
--backends <URLs> string 쉼표로 구분된 백엔드 URL
--backend-url <URL> string 단일 백엔드 URL (지원 중단)

서버 설정

인수 타입 설명
--bind <ADDRESS> string 서버 바인드 주소
--connection-pool-size <SIZE> integer HTTP 연결 풀 크기

로드 밸런싱

인수 타입 설명
--selection-strategy <STRATEGY> string 로드 밸런싱 전략: RoundRobin (기본값), WeightedRoundRobin, LeastLatency, Random, ConsistentHash, PrefixAwareHash

헬스 체크 설정

인수 타입 설명
--disable-health-checks flag 헬스 모니터링 비활성화
--health-check-interval <SECONDS> integer 헬스 체크 간격
--health-check-timeout <SECONDS> integer 헬스 체크 타임아웃
--unhealthy-threshold <COUNT> integer 비정상 판정까지의 실패 횟수
--healthy-threshold <COUNT> integer 정상 판정까지의 성공 횟수

설정 유틸리티

인수 타입 설명
--migrate-config-file <FILE> path 라우터를 시작하지 않고 설정 파일 문제를 마이그레이션/수정. 백업을 만들고 모든 변경 사항을 보고 (YAML, TOML 지원)
--dry-run flag --migrate-config-file과 함께 사용해 변경 사항을 적용 없이 미리보기

컨테이너 헬스 체크

인수 타입 설명
--health-check flag 실행 중인 서버의 상태를 확인하고 종료 (0 = 정상, 1 = 비정상). Docker HEALTHCHECK 용도
--health-check-url <URL> string 확인할 헬스 엔드포인트 (기본값: http://localhost:8080/health)

서브커맨드

명령 설명
auth login --backend <NAME> 지정한 백엔드(auth.type이 oauth여야 함)에 대해 OAuth 디바이스 인가 플로를 실행하고 토큰을 auth.oauth.token_store에 저장. OAuth 백엔드 설정은 서버 & 백엔드를 참조하세요.
config validate <FILE> YAML/TOML을 검증하고 구조화된 JSON 보고서를 출력. 유효하지 않으면 0이 아닌 코드로 종료
config generate --template <NAME> [--output <FILE>] 내장 템플릿 중 하나를 생성: minimal, development, multi-provider, production-ha, api-gateway, kv-cache-optimized, smart-routing, disaggregated, cost-optimized, sglang, ab-testing
config diff <A> <B> 두 설정의 유효 값을 비교. 비밀 값으로 추정되는 값은 마스킹
config show [--resolved] <FILE> 정규화된 설정을 출력. --resolved는 공급자 기본값과 지원되는 필드별 환경 변수 확장을 적용하므로 평문 자격 증명이 출력될 수 있음
metadata download (별칭 metadata update) 해석된 기본 메타데이터 파일을 갱신하거나 --all / --dir <DIR>로 원본 URL이 있는 모든 카탈로그 조각을 갱신. 아래 모델 메타데이터 내려받기 참조
metadata show [--resolved] [--json] 기본 파일과 모든 model-metadata.d/ 드롭인을 합친 메타데이터를 적용된 레이어·건너뛴 파일과 함께 출력. --resolved는 원본 병합 대신 라우터가 실제로 쓰는 타입 문서를 출력. 아래 계층형 모델 메타데이터 참조
status [--url <URL>] [--watch] [--json] 실행 중인 라우터의 Admin API를 읽어 터미널 대시보드를 출력. 백엔드 상태와 회로 차단기 상태, 전체 카운터, 요청 수 기준 상위 모델을 보여줌. 아래 상태 대시보드 참조
usage [--url <URL>] [--by <DIM>] [--series] [--json] 실행 중인 라우터의 Admin API를 읽어 토큰 사용량 보고서를 출력. 모델, 백엔드, API 키, 사용자별로 한 행씩 보여주며, 일별 토큰 차트를 덧붙일 수 있음. 아래 사용량 보고서 참조
mcp-serve stdio에서 설정 도우미 MCP 서버 실행. 기본 빌드와 공식 릴리스에 포함된 mcp Cargo 기능 필요

모델 메타데이터 내려받기

model-metadata.yaml과 model-metadata.d/의 벤더·개발자별 조각에는 라우터 전체가 참조하는 가격, 컨텍스트 윈도, 기능, thinking 패턴, /v1/models 응답 기본값이 담겨 있습니다. 릴리스 아카이브·Debian 패키지·컨테이너 이미지는 호환용 기본 카탈로그와 분할 조각을 모두 포함하며, metadata download로 바이너리 릴리스 사이에도 갱신할 수 있습니다.

export CONTINUUM_GITHUB_TOKEN="$(gh auth token)"    # 저장소가 비공개인 동안 필수

continuum-router metadata download                  # main을 받아 해석된 경로에 설치
continuum-router metadata download --check          # 최신 여부만 확인하고 쓰지 않음
continuum-router metadata download --ref v1.15.5    # 릴리스 태그로 고정
continuum-router metadata download --output ./model-metadata.yaml
continuum-router metadata update --all               # 기본 파일과 설정된 드롭인 경로
continuum-router metadata update --dir ./model-metadata.d

업스트림이 관리하는 파일은 최상위에 HTTPS source_url을 선언합니다. 대상 파일이 이미 있고 --url을 주지 않은 단일 파일 갱신은 그 원본을 사용하며, 새 파일이나 source_url이 없는 예전 파일만 내장 저장소 URL을 사용합니다. --ref는 continuum-router raw-content 저장소 아래 원본의 ref 부분만 바꾸고 제3자 원본은 그대로 둡니다.

--all은 해석된 기본 파일과 설정된 드롭인 탐색 경로 전체를 훑습니다. --dir <DIR>는 명시적 드롭인 디렉터리를 추가하며 반복할 수 있습니다. source_url이 없는 파일은 운영자 로컬 파일이므로 벌크 모드가 skipped_local로 보고하고 절대 쓰지 않습니다. --check, --force, --no-backup, --ref, --json은 추적 파일 모두에 동일하게 적용되며, --check는 하나라도 다르면 2로 종료합니다. --url과 --output은 단일 파일 옵션이라 벌크 모드와 함께 쓸 수 없습니다.

벌크 계획은 런타임 조립과 같은 상한을 사용합니다. 기본 파일과 드롭인을 합쳐 최대 64개, 카탈로그 데이터 총합은 최대 8 MiB입니다. source_url이 없는 로컬 파일도 바이트 예산을 차지하고 다운로드한 교체본은 남은 예산을 공유합니다. 추적 대상 교체본을 모두 내려받아 스키마 검증을 마친 뒤에야 첫 파일을 설치하므로, 뒤쪽 원본이 실패하거나 상한을 넘더라도 앞쪽 파일만 갱신되어 카탈로그가 불일치하는 상태가 되지 않습니다. 단일 파일 모드도 8 MiB를 넘는 응답을 거부합니다.

인증

continuum-router 저장소는 비공개라서 기본 소스에 인증 없이 요청하면 실제로 존재하는 ref까지 포함해 모든 ref가 404로 돌아옵니다. 읽기 권한이 있는 GitHub 토큰을 아래 변수 중 하나로 넘기세요. 순서대로 확인해서 비어 있지 않은 첫 값을 씁니다.

변수 설명
CONTINUUM_GITHUB_TOKEN 권장. 라우터 자체의 CONTINUUM_* 환경 변수 체계와 맞으므로, 호스트의 모든 도구와 토큰을 공유하지 않고 라우터에만 한정할 수 있음
GITHUB_TOKEN CI 러너가 이미 내보내는 이름
GH_TOKEN GitHub CLI가 이미 내보내는 이름

앞뒤 공백은 잘라내고 공백만 있는 값은 설정되지 않은 것으로 취급하므로, 실수로 남은 export GITHUB_TOKEN=이 실제 GH_TOKEN을 가리지 않습니다.

--token 플래그는 일부러 두지 않았습니다. 명령줄 인수로 넘긴 비밀은 셸 히스토리에 남고 같은 머신의 모든 사용자에게 ps 출력으로 보입니다.

토큰은 각 요청 호스트가 정확히 raw.githubusercontent.com 또는 api.github.com일 때만 붙습니다. 미러를 가리키는 --url이나 파일의 source_url, raw.githubusercontent.com.evil.test 같은 유사 호스트에는 절대 전달되지 않습니다. 리다이렉트는 HTTPS 안에서 최대 10홉만 허용합니다. 리다이렉트 요청에는 Referer 헤더를 만들지 않으므로 서명된 원본 URL의 query 자격 증명이 다음 origin에 노출되지 않습니다. 토큰은 로그에도 출력에도 남지 않으며 보고서에는 값이 아니라 토큰을 읽어온 변수 이름만 나옵니다.

404가 나면 어느 쪽인지 메시지로 구분됩니다. 토큰이 없으면 세 변수 이름과 함께 저장소가 비공개일 수 있다고 알리고 토큰이 있으면 ref나 경로가 없거나 토큰에 접근 권한이 없을 수 있다고 알립니다.

--url에는 비공개 미러용 basic 인증 정보를 담을 수 있습니다(https://ci:token@mirror.internal/model-metadata.yaml). 이 값은 Authorization: Basic 헤더로 전송되지만, 표시되는 모든 원본(사람이 읽는 보고서, --json의 source_url 필드, 계층 보고서, 오류 메시지)에서는 userinfo와 query 전체, fragment를 제거합니다. 전송 오류에서도 reqwest의 원래 요청 URL을 버립니다. 실제 fetch는 수정하지 않은 URL을 사용하지만 비밀번호, 서명 query 자격 증명, fragment 데이터는 CI 로그나 보관된 보고서에 남지 않습니다.

플래그

플래그 기본값 동작
--ref <GIT_REF> main 내려받을 브랜치 또는 태그. --url과 함께 쓸 수 없음
--url <URL> --ref에서 유도 미러나 폐쇄망 설치를 위한 전체 HTTPS 소스 오버라이드
--output <FILE> 아래 규칙으로 해석 명시적 대상 경로
--all 꺼짐 기본 파일과 설정된 드롭인 탐색 경로의 source_url 파일을 모두 갱신
--dir <DIR> 없음 지정한 드롭인 디렉터리의 source_url 파일을 갱신. 반복 가능
--check 꺼짐 내려받아 비교만 하고 쓰지 않음
--force 꺼짐 콘텐츠 해시가 같아도 다시 씀
--no-backup 꺼짐 교체 대상 파일의 <file>.bak 사본을 만들지 않음
--timeout <SECONDS> 30 요청 전체 제한 시간
--json 꺼짐 사람이 읽는 보고서 대신 기계가 읽는 보고서 출력

대상 경로 해석에는 전역 -c/--config와 --model-metadata를 그대로 씁니다.

대상 경로 해석

대상은 다음 순서로 정해지며 성공하든 실패하든 언제나 출력됩니다.

  1. --output <FILE>
  2. 전역 --model-metadata <FILE> (틸드 확장 적용)
  3. 불러온 설정의 model_metadata_file (틸드 확장 적용)
  4. 발견한 설정 파일 옆의 model-metadata.yaml
  5. ~/.config/continuum-router/model-metadata.yaml

--model-metadata가 설정 필드보다 위에 있는 이유는 라우터 본체에서 하는 일이 바로 그것이기 때문입니다. 이 플래그는 model_metadata_file을 오버라이드합니다. 따라서 continuum-router --model-metadata /etc/cr/meta.yaml metadata download는 해당 라우터 인스턴스가 실제로 읽을 경로에 파일을 설치합니다.

상위 디렉터리가 없으면 만듭니다.

안전 동작

  • 내려받은 내용은 디스크를 건드리기 전에 메타데이터 스키마로 파싱·검증합니다. 형식이 깨졌거나 스키마에 맞지 않으면 0이 아닌 코드로 종료하고 기존 파일은 바이트 단위로 그대로 둡니다.
  • 교체는 대상 디렉터리 안의 임시 파일을 거쳐 원자적으로 이뤄지며 기존 파일의 권한 모드를 유지합니다(새 파일은 0644).
  • --no-backup을 주지 않으면 이전 파일을 <file>.bak으로 남깁니다.
  • SHA-256으로 내용을 비교하므로 바뀐 것이 없으면 --force 없이는 쓰지 않고 건너뜁니다.
  • HTTPS 소스만 받아들이며 HTTPS를 벗어나는 리다이렉트는 거부합니다.
  • 8 MiB를 넘는 응답은 거부합니다.

종료 코드

코드 의미
0 정상적으로 썼거나 이미 최신
1 내려받기·검증·쓰기 실패
2 --check 전용: 업데이트가 있음

2가 따로 있어서 cron이나 CI가 오래된 사본을 오류로 취급하지 않고 분기할 수 있습니다.

실행 중인 라우터에 미치는 영향

파일을 갱신해도 실행 중인 라우터가 핫 리로드하지는 않습니다. 설정 감시기는 설정 파일 경로만 감시하고 model_metadata_file과 model_metadata_dirs 모두 admin 설정 스키마에서 requires_restart로 표시되어 있으므로, 새 메타데이터를 적용하려면 라우터를 재시작하거나 설정 리로드를 걸어야 합니다. 명령은 실행할 때마다 이 안내를 출력합니다.

계층형 모델 메타데이터

model_metadata_file은 0층일 뿐 전부가 아닙니다. 배포 카탈로그는 낮은 숫자 접두사의 벤더·개발자별 조각으로 나뉘고, 운영자 파일은 보통 90- 같은 더 늦은 접두사를 씁니다. 벌크 갱신은 source_url로 명시적으로 참여한 파일만 건드립니다.

탐색 경로

우선순위가 낮은 것부터 나열하며, 더 구체적인 위치가 나중에 적용되어 우선합니다.

  1. model_metadata_file (기본 파일, 틸드 확장 적용)
  2. /etc/continuum-router/model-metadata.d/
  3. ~/.config/continuum-router/model-metadata.d/
  4. ./model-metadata.d/
  5. model_metadata_dirs에 적은 디렉터리 전부, 적은 순서대로

model_metadata_dirs는 관례 위치를 대체하지 않고 보강합니다. 또한 항목을 그대로 쓰기 때문에 뒤에 model-metadata.d를 붙이지 않습니다. 쿠버네티스 ConfigMap을 /etc/router/metadata/에 마운트했다면 그 경로를 그대로 적으면 됩니다. 전역 --model-metadata 플래그의 의미도 그대로입니다. 이 플래그는 기본 파일을 지정할 뿐 드롭인을 끄지 않습니다.

전형적인 배치는 다음과 같습니다.

/etc/continuum-router/model-metadata.yaml        # 벤더 기준 파일, 덮어써도 안전
/etc/continuum-router/model-metadata.d/
    10-alibaba.yaml                              # 배포 파일, source_url 있음
    23-openai.yaml                               # 배포 파일, source_url 있음
    50-negotiated-pricing.yaml                   # 로컬 파일, source_url 없음
    90-local-overrides.yaml

파일 선택 규칙

한 디렉터리 안에서는 이렇게 동작합니다.

  • 확장자가 .yaml 또는 .yml인 일반 파일만 읽으며 확장자는 대소문자를 구분하지 않습니다
  • 파일 이름의 사전순으로 적용합니다. 10-/50-/90- 숫자 접두사 관례가 동작하는 이유입니다
  • 하위 디렉터리로는 재귀하지 않습니다
  • 점으로 시작하는 파일, *~, *.swp는 건너뜁니다. 편집기 백업과 vim 스왑 파일이 여기에 해당합니다
  • 심볼릭 링크는 따라가되 최종 대상이 일반 파일이어야 합니다
  • 디렉터리가 없어도 오류가 아닙니다

병합 규칙

레이어는 역직렬화 이전 단계인 파싱된 YAML에서 병합합니다.

  • 매핑 대 매핑: 키 단위 재귀 병합. 상위 레이어가 적지 않은 키는 하위 레이어에서 물려받습니다
  • 시퀀스: 통째로 교체하며 합집합을 만들지 않습니다
  • 스칼라(명시적 null 포함): 교체합니다
  • models 항목은 id를 키로 씁니다. 이미 있는 id는 기존 항목에 병합하고 새 id는 뒤에 덧붙입니다
  • response_defaults는 매핑이므로 필드 단위로 병합됩니다

역직렬화 전에 병합하기 때문에 좁은 범위의 오버라이드가 가능합니다. responses_only를 언급하지 않은 오버라이드 파일이 물려받은 true를 지워버릴 수 없고 나중에 메타데이터 스키마에 필드가 추가되어도 병합 코드를 손대지 않아도 됩니다.

시퀀스를 합집합이 아니라 교체로 정한 것은 의도적입니다. aliases와 capabilities가 시퀀스인데, 합집합이라면 물려받은 항목을 지울 방법이 없습니다.

벤더 기준 파일이 이렇고

models:
  - id: gpt-5
    aliases: [gpt-5-latest]
    metadata:
      display_name: GPT-5
      capabilities: [chat, reasoning, vision]
      pricing: { input_tokens: 1.25, output_tokens: 10.0 }

model-metadata.d/50-negotiated-pricing.yaml이 이렇다면

models:
  - id: gpt-5
    metadata:
      pricing: { input_tokens: 0.90, output_tokens: 7.20 }

결과는 display_name, capabilities, aliases가 계속 업스트림을 따라가면서 pricing만 계약 단가로 바뀝니다. 운영자는 전체 사본을 떠안고 낡아가는 대신 네 줄만 관리하면 됩니다.

종료된 모델

모든 제공 경로(공급자 자체 API, Azure OpenAI, Vertex AI)에서 서비스가 종료된 모델은 기본 카탈로그에서 저장소 루트의 model-metadata-retired.yaml로 분리됩니다. 이를 통해 /v1/models 메타데이터와 스마트 라우팅 프로필 추론은 요청이 실제로 도달할 수 있는 모델만 다룹니다.

라우터는 model-metadata-retired.yaml을 자동으로 읽지 않습니다. 이 파일은 model-metadata.yaml과 동일한 스키마를 사용하며 각 항목에 서비스 종료 위치와 시점을 기록합니다. 자체 호스팅 OpenAI 호환 게이트웨이 등으로 해당 모델 ID를 계속 서빙하는 환경이라면, 이 파일 전체나 필요한 항목만 model-metadata.d/ 디렉터리에 복사해 메타데이터를 복원할 수 있습니다. 어느 한 경로에서라도 여전히 제공되는 모델은 기본 카탈로그에 유지되며, 두 파일에 동일한 ID나 별칭이 중복으로 존재하지 않습니다.

메타데이터 계층 바깥의 우선순위

바뀌지 않습니다. 백엔드 model_configs가 여전히 병합된 메타데이터보다 우선하고 병합된 메타데이터가 내장 OpenAI 레지스트리보다 우선합니다. 계층화는 메타데이터 계층을 조립하는 방식만 다룹니다.

모델 아티팩트 식별 정보

모델 id는 클라이언트가 요청한 이름일 뿐, 어떤 가중치가 응답했는지는 말해 주지 않습니다. 두 키로 운영자가 모델이 어떤 아티팩트에서 서비스되는지 선언하면, 같은 id를 서로 다른 가중치(다른 양자화, 다른 리비전, 다시 내보낸 체크포인트)로 서비스하는 두 백엔드를 사용량 레코드에서 구별할 수 있습니다.

# model-metadata.d/10-private-models.yaml
models:
  - id: qwen3.8-27b
    metadata:
      artifact:
        digest: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
        registry_version: v2.1-fp8
        revision: 4c2e9a1
        source: hf://Qwen/Qwen3.8-27B-FP8
      draft_model:
        name: qwen3.8-0.6b
        artifact:
          digest: blake3:5e2b0a9c1f7d3e8b4a6c0d2f9e1b7a3c5d8f0e2a4c6b8d0f1e3a5c7b9d0f2e4a
키 규칙
artifact.digest sha256: 또는 blake3: 뒤에 16진수 64자리. 대문자도 받으며 모든 곳에서 소문자로 표시
artifact.registry_version, artifact.revision 자유 텍스트, 1~128바이트, 제어 문자 불가
artifact.source 512바이트 이하의 URI(hf://, oci://, https://, s3://, file://). 사용자 이름이나 비밀번호를 담은 source는 거부되며 오류 메시지에서 자격 증명은 가려짐. javascript:, data:, vbscript: 스킴과 <, >, " 문자도 거부됨. source는 GET /v1/models/{model}로 모든 클라이언트에게, GET /admin/models로 관리자에게 보이는 공개 카탈로그 데이터이므로 사전 서명된(presigned) URL이나 토큰이 담긴 URL(S3, GCS, Azure의 사전 서명 URL, ?token=이나 ?sig= 쿼리)은 절대 쓰지 말고 hf://org/model이나 oci://registry/repo@sha256:... 같은 안정적인 공개 위치를 쓸 것
draft_model.name 드래프트의 서비스 이름, 1~256바이트, 공백만으로는 불가. 대상 id와 같아도 됨
draft_model.artifact 드래프트 자신의 아티팩트, artifact와 같은 규칙

식별 정보는 백엔드와 모델별로, 키 하나씩 해석됩니다. 백엔드 자신의 model_configs 항목이 키를 설정하면 그것이 우선하고 그렇지 않으면 병합된 메타데이터 계층의 값을 씁니다. artifact를 설정한 백엔드는 공유 아티팩트를 통째로 대체하며(하위 필드를 공유 값과 섞지 않음), draft_model은 따로 해석되므로 백엔드가 다이제스트만 바꾸고 공유 드래프트는 그대로 물려받을 수 있습니다. 클라우드 모델에는 운영자가 보증할 수 있는 아티팩트가 없으므로 내장 OpenAI 레지스트리는 참조하지 않습니다.

백엔드에 도달한 모든 사용량 레코드는 해석된 model_digest와 draft_model을 담습니다(사용량 레코드의 아티팩트 식별 정보 참고). draft_model은 드래프트의 이름을 밝힐 뿐 아무것도 세지 않습니다. 엔진은 수락된 토큰만 보고하며 그 토큰은 대상 모델에 귀속됩니다.

잘못된 형식의 값이 처리되는 방식은 선언 위치에 따라 다릅니다.

  • backends[].model_configs에 선언된 경우: 설정 로드 오류로 처리되며, config validate가 모델 이름과 함께 정확한 경로(backends[1].model_configs[0].metadata.artifact.digest)를 보고합니다.
  • model-metadata.yaml이나 드롭인 파일에 선언된 경우: 범위를 벗어난 가격과 동일하게 경고 전용 규칙을 따릅니다. 모델 서빙은 유지되고 모델 이름을 포함한 경고가 한 번 출력되며, 잘못된 artifact(또는 draft_model) 블록만 제외되어 /v1/models, /admin/models, 사용량 레코드에 전달되지 않습니다. metadata show --resolved는 원본 값을 그대로 출력하면서 위반 사항을 identity_warnings에 나열하며, metadata download는 잘못된 식별 정보가 포함된 카탈로그를 거부합니다.

config validate는 또한 vllm, sglang, tensorrt-llm 백엔드 항목이 대상이나 드래프트의 다이제스트 없이 draft_model을 선언하면, 그리고 model_configs가 같은 모델 id를 백엔드마다 다른 다이제스트로 선언하면 id마다 한 번(백엔드가 의도적으로 다른 빌드를 서비스할 때는 예상된 상황) 실패 없이 경고합니다.

실패 처리와 한도

읽기·파싱·검증에 실패한 드롭인 파일은 경고와 함께 건너뛰고 나머지 탐색 경로는 정상적으로 로드하므로, 잘못된 운영자 파일 하나가 실행 중인 라우터를 중단시키지 않습니다. 반면 기본 model_metadata_file이 잘못된 경우는 치명적인 로드 오류입니다.

스키마 실패 원인은 해당 오류를 처음 도입한 레이어 하나에만 귀속됩니다. 특정 레이어를 적용하기 직전까지 병합 결과가 스키마를 만족했을 때만 그 레이어를 원인으로 지목하므로, 기본 파일 자체가 스키마를 위반하면 그 위의 드롭인 파일들이 아니라 기본 파일이 원인으로 보고됩니다. metadata show는 이 정보를 Schema failure introduced by 항목으로 출력합니다.

파일은 최대 64개, 합계 크기는 최대 8 MiB까지 조립합니다. 한도를 넘으면 잘라내지 않고 오류로 처리합니다. 오버라이드 집합을 일부만 조용히 적용하는 쪽이 조립을 거부하는 쪽보다 나쁘기 때문입니다.

핫 리로드 도중 조립이 치명적으로 실패하면 메타데이터 계층뿐 아니라 리로드 전체가 중단됩니다. 라우터는 조립 실패를 원인으로 기록하고, 직전에 로드한 설정과 모델 메타데이터를 그대로 유지한 채 서빙을 이어갑니다. 이는 빈 메타데이터 캐시가 발행되어 가격, 컨텍스트 윈도, 기능, /v1/models 응답 기본값이 재시작 없이 백엔드 보고값으로 조용히 되돌아가는 일을 막기 위함입니다. model-metadata.d/ 디렉터리에 65번째 파일을 추가하거나 총합이 8 MiB를 넘게 만드는 파일 하나를 추가하는 것만으로도 다음 리로드에서 이 치명적 조립 실패가 발생하므로 주의하세요.

결과 확인

건너뛴 파일은 클라이언트 쪽에서 보이지 않으므로, 확인 수단은 metadata show입니다.

continuum-router metadata show              # 원본 병합 결과와 레이어 보고서
continuum-router metadata show --resolved   # 라우터가 실제로 쓰는 타입 문서
continuum-router metadata show --json       # 같은 보고서를 기계가 읽는 객체로
  • 공통 출력: 두 모드 모두 탐색 경로, 적용 순서대로의 레이어 목록(가려진 source_url, 후속 레이어가 덮어쓴 모델 ID와 원본 파일 포함), 건너뛴 파일과 사유를 출력합니다. 머리말은 YAML 주석으로 작성되므로 metadata show > merged.yaml로 저장하면 라우터가 바로 읽을 수 있는 유효한 YAML 파일이 됩니다.
  • 기본 모드 vs. --resolved 모드: --resolved 없이 실행하면 스키마가 무시하는 키까지 포함한 원본 병합 결과를 그대로 출력하므로 오타를 쉽게 확인할 수 있습니다. --resolved를 붙이면 병합 결과를 역직렬화하고 response_defaults를 검증·정제한 뒤 타입 구조를 다시 직렬화합니다. 이때 아티팩트 다이제스트는 정규화된 소문자 표기로 출력되며, 로드 시 제외되는 잘못된 아티팩트 식별 정보는 원본 값과 함께 # Artifact identity warnings(--json에서는 identity_warnings) 아래에 나열됩니다.

metadata show는 탐색 경로를 조립할 수 있었다면 건너뛴 파일이 있어도 0으로 종료하고 기본 파일을 읽을 수 없거나 한도를 넘긴 것 같은 치명적 실패에만 1로 종료합니다.

찾아낸 설정 파일이나 --config로 직접 지정한 설정 파일을 찾지 못했거나 불러오지 못했다면, metadata show는 그 설정의 model_metadata_file과 model_metadata_dirs가 빠진 탐색 경로를 아무 말 없이 보여주는 대신 이를 알립니다. 사람이 읽는 보고서에는 # WARNING: 머리글 줄로, --json에는 config_warning 필드로 나타납니다.

metadata download와의 관계

벌크 플래그가 없으면 metadata download는 해석된 파일 하나를 갱신하고 그 파일의 source_url을 우선합니다. --all이나 --dir을 쓰면 원본이 선언된 기본 파일과 드롭인 파일을 모두 갱신하고 source_url이 없는 파일은 명시적으로 건너뜁니다. 이 파일별 참여 여부가 안전 경계이므로 배포 벤더 조각은 언제든 재생성할 수 있고 운영자 파일은 손대지 않습니다.

드롭인 변경도 핫 리로드되지 않습니다. model_metadata_dirs는 model_metadata_file과 마찬가지로 admin 설정 스키마에서 requires_restart입니다.

상태 대시보드

continuum-router status는 이미 실행 중인 라우터의 Admin API를 읽어 대시보드를 출력합니다. 로컬 설정 파일은 읽지 않으므로 라우터에 접속할 수 있는 어느 호스트에서든 사용할 수 있습니다.

대시보드는 세 부분으로 구성됩니다.

  • Backends: 백엔드별 헬스 상태, 회로 차단기 상태, 요청 수와 실패 수, 평균 지연 시간.
  • Models: 요청 수 기준 상위 모델과 점유율 막대, 토큰 합계, 초당 토큰 수, 평균 지연 시간.
  • Overall: 전체 요청 수와 성공률, 프롬프트 토큰과 완성 토큰, 지연 시간 백분위수, 회로 차단기 요약.

이 명령은 GET /version, GET /admin/stats, GET /admin/circuit/all을 호출합니다. 따라서 대상 라우터는 admin 기능을 포함해 빌드되어 있어야 합니다. 기본 빌드와 공식 릴리스에는 포함되어 있습니다.

옵션 기본값 설명
--url <URL> http://localhost:8080 조회할 라우터의 기본 URL
--token <TOKEN> 없음 Admin API용 Bearer 토큰. 지정하지 않으면 CONTINUUM_ADMIN_TOKEN을 사용
--watch 꺼짐 --interval초마다 화면을 다시 그리고 요청률 스파크라인을 표시. Ctrl+C로 종료
--interval <SECONDS> 5 --watch 모드의 갱신 간격. 1 이상이어야 함
--window <WINDOW> 전체 기간 /admin/stats에 전달할 통계 구간. 예: 1h, 24h, 7d
--top <N> 10 모델 표에 표시할 행 수
--json 꺼짐 원본 응답을 하나의 JSON 객체로 출력. --watch와 함께 쓰면 갱신마다 객체를 하나씩 출력(NDJSON)
--timeout <SECONDS> 10 Admin API 호출별 제한 시간. 1 이상이어야 함

인증 방식은 admin.auth.method를 따릅니다.

  • api_key (기본값): admin.auth.required_scope에 지정한 스코프(기본값 admin)가 있는 API 키를 전달합니다.
  • bearer: 설정된 admin.auth.bearer_token을 전달합니다.
  • none 또는 ip_whitelist: 토큰이 필요 없습니다.
  • basic: 이 명령에서는 지원하지 않습니다.

--token보다 CONTINUUM_ADMIN_TOKEN을 쓰는 것이 좋습니다. 명령줄 인수는 프로세스 목록을 통해 다른 로컬 사용자도 볼 수 있지만, 프로세스의 환경 변수는 소유자만 읽을 수 있습니다. 토큰은 출력되지 않으며, 클라이언트가 리디렉션을 따라가지 않으므로 토큰이 다른 출처로 전달되지도 않습니다.

스냅샷을 가져와 출력하면 종료 코드 0, 라우터에 연결할 수 없거나 라우터가 오류로 응답하면 1로 종료합니다. 401이나 403 응답에는 토큰 관련 안내가 덧붙습니다. --watch 모드에서는 조회에 실패해도 오류와 함께 화면을 다시 그리고 계속 조회하므로, 멈춘 라우터의 상태도 화면에서 확인할 수 있습니다.

표준 출력이 터미널이 아니거나 NO_COLOR가 설정되어 있으면 색상을 끕니다. 모델 이름과 백엔드 이름의 제어 문자는 다른 문자로 바꿔 출력하므로, 업스트림 백엔드가 보고한 이름으로 터미널 이스케이프 시퀀스를 주입할 수 없습니다.

사용량 보고서

continuum-router usage는 실행 중인 라우터의 Admin API를 읽어 모델, 백엔드, API 키, 사용자 가운데 하나를 기준으로 행을 나눈 토큰 사용량 보고서를 출력합니다. status와 마찬가지로 로컬 설정 파일은 읽지 않습니다.

--by 엔드포인트 열
models (기본값) GET /admin/stats 요청 수, 실패 수, 토큰 점유율 막대, 토큰 수, 초당 토큰 수, 평균 지연 시간
backends GET /admin/stats 요청 수, 실패 수, 요청 점유율 막대, 평균 지연 시간. 백엔드 카운터에는 토큰이 없으므로, 토큰 열은 -로 출력
api-keys GET /admin/stats/api-keys models와 같은 열. 행 키는 파생된 api_key_id이며, 원본 키는 출력하지 않음
users GET /admin/stats/users models와 같은 열. 행 키는 사용자 id

행은 토큰 수(backends는 요청 수)가 많은 순서로 정렬하고 --top은 앞의 N개만 남깁니다. --series를 주면 GET /admin/stats/series의 전역 일별 토큰 시계열을 UTC 기준 하루당 막대 하나로 덧붙이며, 막대는 프롬프트 토큰(█)과 완성 토큰(▓)으로 나뉩니다. --series-days를 크게 주더라도 라우터는 설정된 시계열 보존 기간까지만 응답합니다.

옵션 기본값 설명
--url <URL> http://localhost:8080 조회할 라우터의 기본 URL
--token <TOKEN> 없음 Admin API용 Bearer 토큰. 지정하지 않으면 CONTINUUM_ADMIN_TOKEN을 사용
--by <DIM> models models, backends, api-keys, users 중 하나
--window <WINDOW> 전체 기간 models와 backends에 적용할 통계 구간. 예: 1h, 24h. API 키별·사용자별 카운터는 전체 기간 값이라 api-keys나 users와 함께 쓰면 안내를 출력하고 전달하지 않음
--top <N> 15 표시할 행 수
--series 꺼짐 일별 토큰 차트를 덧붙임
--series-days <DAYS> 30 --series가 조회할 기간(일). 1 이상이어야 함
--json 꺼짐 원본 응답을 하나의 JSON 객체(window, uptime_seconds, stats, --series를 주면 series)로 출력
--timeout <SECONDS> 10 Admin API 호출별 제한 시간. 1 이상이어야 함

인증 방식과 종료 코드, 제어 문자 처리는 status와 같습니다. 엔드포인트 중 하나를 제공하지 않는 라우터(이전 버전이거나 admin 기능 없이 빌드한 경우)를 대상으로 하면 빈 보고서를 출력하지 않고 1로 종료합니다. 엔드포인트가 빈 목록으로 응답하면 no usage recorded를 출력합니다.

CLI 사용 예제

# 설정 파일과 오버라이드 사용
continuum-router --config config.yaml --bind "0.0.0.0:9000"

# 백엔드 임시 오버라이드
continuum-router --config config.yaml --backends "http://localhost:11434"

# 사용자 정의 모델 메타데이터 파일 사용
continuum-router --config config.yaml --model-metadata /path/to/custom-metadata.yaml

# 틸드 확장과 함께 모델 메타데이터 사용
continuum-router --model-metadata ~/configs/model-metadata.yaml

# 테스트용 헬스 체크 설정 조정
continuum-router --config config.yaml --health-check-interval 10

# 샘플 설정 생성
continuum-router --generate-config > my-config.yaml

# 검증하고 목적별 템플릿 생성
continuum-router config validate my-config.yaml
continuum-router config generate --template production-ha --output production.yaml

# 설정 조회 또는 비교 (`--resolved` 출력에는 비밀 값이 포함될 수 있음)
continuum-router config show my-config.yaml
continuum-router config diff my-config.yaml production.yaml

# 구버전 설정 파일 마이그레이션 (미리보기 후 적용)
continuum-router --migrate-config-file config.yaml --dry-run
continuum-router --migrate-config-file config.yaml

# Docker HEALTHCHECK 프로브
continuum-router --health-check --health-check-url http://localhost:8080/health

# ChatGPT/Codex 백엔드용 OAuth 디바이스 플로 로그인
continuum-router --config config.yaml auth login --backend chatgpt

# MCP 클라이언트에 설정 도우미 등록
claude mcp add continuum-router-config -- continuum-router mcp-serve

# 모델 메타데이터 갱신, 또는 로컬 사본이 오래됐는지 확인 (종료 코드 2 = 업데이트 있음)
export CONTINUUM_GITHUB_TOKEN="$(gh auth token)"
continuum-router metadata download
continuum-router metadata download --check --json

# 이 라우터 인스턴스가 읽는 경로에 설치
continuum-router --model-metadata /etc/continuum-router/model-metadata.yaml metadata download

# model-metadata.d 계층까지 반영된 실제 적용 메타데이터 확인
continuum-router metadata show --resolved
continuum-router metadata show --json

# 실행 중인 라우터의 대시보드. 관리 토큰이 프로세스 목록에 노출되지 않게 환경 변수로 전달
export CONTINUUM_ADMIN_TOKEN="<admin-scoped API key>"
continuum-router status --url http://router.internal:8080
continuum-router status --watch --interval 2 --window 1h
continuum-router usage --by api-keys --top 20
continuum-router usage --window 24h --series --series-days 14