Admin REST API¶
Continuum Router는 바이너리가 admin 기능으로 빌드된 경우 /admin 아래에 관리 API를 마운트합니다. 기본 기능 세트에는 admin이 포함됩니다.
Admin API는 신뢰할 수 있는 관리 네트워크용입니다. 기본 동작은 실패 시 차단입니다. admin.auth가 없으면 모든 Admin 요청이 401 Unauthorized를 반환합니다. method가 없는 admin.auth 블록은 기본적으로 api_key를 사용하며 admin 스코프를 가진 키를 요구합니다. 의도적으로 인증하지 않는 개발 인스턴스를 실행하려면 admin.auth.method: none을 명시하세요. 공유 네트워크에서는 이 선택을 사용하지 마세요.
인증¶
Bearer 토큰¶
bearer_token은 16자 이상이어야 합니다. allowed_ips는 선택 사항이며 개별 IPv4/IPv6 주소와 CIDR 범위를 받습니다.
HTTP Basic¶
IP 허용 목록¶
allowed_ips는 method: bearer 또는 method: basic과 함께 사용할 수도 있습니다. Unix 소켓 연결에는 피어 IP가 없으므로 IP 검사를 건너뜁니다.
API 키¶
# Authorization: Bearer와 X-API-Key를 모두 허용합니다
curl -H "Authorization: Bearer $ADMIN_API_KEY" http://127.0.0.1:8080/admin/health
curl -H "X-API-Key: $ADMIN_API_KEY" http://127.0.0.1:8080/admin/health
method: api_key는 제시된 키를 일반 /v1 인증과 Admin 키 관리(/admin/api-keys)가 사용하는 것과 동일한 API 키 저장소로 검증합니다. 키는 활성 상태이고 만료되지 않았으며 required_scope(기본값 admin)를 가져야 합니다. 저장소를 공유하므로 키를 비활성화, 회전, 만료, 삭제하면 재시작 없이 즉시 Admin API 인증에서도 차단됩니다. 키가 없거나 잘못되면 401, 필요한 스코프가 없는 유효한 키나 화이트리스트에 없는 IP의 요청은 403을 반환합니다. 계층적 보안을 위해 allowed_ips를 method: api_key와 함께 사용할 수 있습니다.
Admin 인증 설정은 Admin 라우터를 구성할 때 캡처됩니다. admin.auth 자체(예: method나 required_scope 변경)를 바꾼 뒤에는 라우터를 재시작하세요. 개별 키 변경은 공유 저장소를 통해 즉시 반영됩니다.
admin.auth는 시작 시 소유되는 설정입니다. Admin 설정 섹션 기록과 전체 문서 가져오기, 적용, 롤백은 이 값을 바꾸는 후보를 거부합니다. 설정 파일을 편집하고 재시작하세요. 내보낸 설정에는 admin.auth가 포함되지 않으므로, 그 밖의 내용을 바꾸지 않은 내보내기 문서를 가져오면 실행 중인 시작 값을 그대로 유지합니다.
감사 로그¶
admin:
audit:
enabled: true
log_level: info
include_headers: false
include_body: false
excluded_headers:
- authorization
- x-api-key
- cookie
감사 로그는 기본적으로 활성화됩니다. Admin 인증을 통과한 Admin 요청마다 admin_audit 줄이 하나 남으며 클라이언트 IP, 마스킹된 자격 증명, 메서드, 경로, 상태 코드, 소요 시간을 담은 JSON 객체입니다.
POST /admin/api-keys와 PUT /admin/api-keys/{id}를 통한 키별 속도 제한 변경은 같은 admin_audit 대상과 설정된 log_level로 api_key_rate_limit_change 줄을 하나 더 남깁니다. include_body 설정과 무관합니다. 이 줄은 key_id와 rate_limit, rate_limit_burst, rate_limit_tokens_per_minute의 before 및 after 값을 담으며, 키 자체는 절대 담지 않습니다. 생성은 제한을 설정할 때만, 수정은 그 값 중 하나가 바뀔 때만 기록합니다.
include_headers: true로 설정하면 excluded_headers에 있는 이름을 뺀 요청 헤더를 추가합니다. 자격 증명 헤더(authorization, x-api-key, api-key, x-goog-api-key, cookie, set-cookie, proxy-authorization, x-auth-token, x-access-token)는 excluded_headers에 없거나 목록이 비어 있어도 항상 제외합니다.
include_body: true로 설정하면 POST, PUT, PATCH, DELETE 요청의 JSON 본문을 16 KiB까지 키 이름 기준으로 비밀 값을 마스킹한 뒤(api_key, token, password, client_secret 등 설정 내보내기가 마스킹하는 키) body로 추가합니다. 일반적인 이름 아래 비밀 값을 담는 두 요청 필드도 마스킹합니다: POST /admin/api-keys의 key(새 키 값)와 POST /admin/config/import, /admin/config/validate의 content(텍스트로 된 전체 설정 문서)입니다. 본문은 핸들러 실행 전에 읽고 그대로 핸들러에 넘기므로 라우트 자체의 본문 크기 제한은 그대로 적용됩니다. 기록하지 않은 본문은 다음과 같이 표시합니다:
body_omitted: "not_json":Content-Type이application/json이 아닙니다. 본문을 읽지 않습니다.body_omitted: "too_large": 본문이 16 KiB를 넘었습니다.body_bytes_read는 캡처를 멈추기 전까지 읽은 바이트 수입니다.body_omitted: "invalid_json": 본문이 비어 있거나 JSON 객체 또는 배열이 아닙니다.body_truncated: true: 마스킹한 형태가 16 KiB보다 길어body를 16 KiB로 잘라 문자열로 저장했습니다.
마스킹은 키 이름으로만 동작하므로 일반 키 아래(예: URL 안)에 들어간 비밀 값은 보낸 그대로 기록됩니다. 요청이 그런 키 아래에 비밀 값이나 사용자 데이터를 담을 수 있다면 본문 로깅을 사용하지 마세요.
엔드포인트 목록¶
아래 경로는 모두 라우터 원점을 기준으로 합니다.
기능 및 상태¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET | /admin/capabilities | 컴파일 시 기능과 런타임 활성 서브시스템 조회 |
GET | /admin/health | 라우터 서비스와 백엔드 상태 요약 |
GET | /admin/models | Admin 인증으로 집계 모델 카탈로그 조회 |
POST | /admin/models/refresh | 집계 모델 카탈로그 갱신 |
POST | /admin/routing/explain | 요청 형태에 대한 읽기 전용 라우팅 결정 설명 (드라이런) |
GET /admin/models의 각 항목은 id, backends, available, status, internal, enabled, aliases, metadata, artifacts를 담습니다. metadata는 GET /v1/models/{model}과 마찬가지로 첫 번째 서빙 백엔드의 메타데이터 항목입니다. artifacts는 백엔드별 아티팩트 식별 정보로 artifact나 draft_model이 해석되는 서빙 백엔드마다 backends 순서대로 원소 하나를 담고 하나도 없으면 빈 배열입니다. 같은 모델 id를 서로 다른 가중치로 서비스하는 두 백엔드를 나란히 보여 주는 유일한 카탈로그 표면입니다.
"artifacts": [
{
"backend": "qwen-fp8",
"artifact": {"digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "revision": "4c2e9a1"},
"draft_model": {"name": "qwen3.8-0.6b"}
},
{
"backend": "qwen-int4",
"artifact": {"digest": "sha256:0b0b000000000000000000000000000000000000000000000000000000000002"},
"draft_model": {"name": "qwen3.8-0.6b"}
}
]
둘 중 하나만 선언한 백엔드에서는 artifact나 draft_model이 null입니다. 해석 규칙은 사용량 레코드에 찍히는 것과 같습니다. 백엔드 자신의 model_configs 키가 메타데이터 계층보다 키 하나씩 우선하며 내장 OpenAI 카탈로그는 참조하지 않습니다. 모델 아티팩트 식별 정보를 참고하세요.
라우팅 결정 설명¶
POST /admin/routing/explain은 모델 하나와 선택적으로 API 키 하나에 대해, 채팅 요청이 어느 백엔드들 사이에서 라우팅되는지와 나머지 후보가 왜 제외되는지를 보고합니다. 라이브 경로가 해당 모델에 대해 찾는 후보를 순회하고, 라이브 경로의 술어를 라이브 경로와 같은 순서로 적용하며, 살아남은 후보에 대해 설정된 전략의 정렬 순서를 미리 보여 줍니다.
드라이런입니다. 모든 검사는 읽기 전용 조회입니다.
- 선택 시점 서킷 검사와 자격 증명 디스패치 게이트를 사용합니다.
- 헬스 스냅샷과 백엔드별 레이트 리밋 조회를 사용합니다.
- 전략 프리뷰는 라운드 로빈 카운터를 전진시키지 않고 읽고, 해시 링을 캐시에 설치하지 않고 지역적으로 만들며,
prepare()를 호출하지 않고 스코어러 상태를 읽습니다.
레이트 리밋 토큰을 소비하지 않고, 서킷을 옮기지 않으며, 서킷 항목을 만들지 않고, 헬스·사용량 카운터를 바꾸지 않으며, 메트릭도 기록하지 않습니다. 같은 상태에서 동일한 호출을 두 번 하면 본문이 바이트 단위로 같습니다. 응답에는 백엔드 URL, 자격 증명, 키 값이 담기지 않습니다.
요청 본문¶
| 필드 | 필수 | 의미 |
|---|---|---|
model | 예 | 요청이 지정하는 모델, 최대 256바이트 |
api_key_id | 아니요 | allowed_models와 allowed_backends를 적용할 API 키의 id. 로컬 키가 우선하며, 없으면 적용된 Continuum Hub 정책 엔벨로프에서 해석합니다 |
request | 아니요 | 입력으로만 사용하는 요청 본문. PrefixAwareHash의 접두사 키, LeastCost의 토큰 추정치, 분류기 형태의 필드에 쓰이며 외부로 전달되지 않습니다 |
응답 필드¶
| 필드 | 의미 |
|---|---|
requested_model | 요청의 model |
resolved_model | backends[].model_configs 별칭 해석 후의 식별자 |
virtual_model | 스마트 라우팅이 활성화된 동안의 가상 모델 이름, 아니면 null |
strategy | 설정된 selection_strategy |
selected | 예측된 다음 선택 { "backend": ..., "model": ... }. 적격 후보가 없으면 null |
deterministic | 샘플링 전략(Random, WeightedRoundRobin)은 실제 선택이 난수를 쓰므로 false, 그 외는 true |
eligible | 전략이 고를 수 있는 백엔드, 후보 순서 |
candidates[] | 후보마다 한 행, 아래에서 설명 |
excluded_models | 스마트 라우터가 자체 풀에서 제외하는 모델 목록, 각 항목은 { "model": ..., "reason": ... }. 스마트 라우팅이 활성화되지 않으면 비어 있습니다 |
policy | api_key_id를 지정한 경우에만 존재하며 아래에서 설명 |
각 candidates[] 행은 backend와 status(eligible 또는 excluded)를 담습니다. 제외된 행은 reason을, rate_limited 행은 retry_after_ms를 함께 담습니다. internal: true 백엔드는 internal: true를 담습니다. 적격 행은 다음을 함께 담습니다.
rank: 보고된 전략 정렬에서의 1부터 시작하는 위치입니다.latency_ms: 라우터가 관찰한 평균 응답 시간이며, 첫 표본 전에는null입니다.score: 스코어러 점수 합계입니다. 등록된 스코어러가 없으면 생략되며,prepare()이후에만 점수를 내는 스코어러는 드라이런이 준비를 호출하지 않으므로 중립값을 보고합니다.
policy는 api_key_id, source(local 또는 hub), key_active, model_permitted, allowed_models, allowed_models_restricted, allowed_backends_restricted, 그리고 허브 키의 경우 tier를 담습니다.
excluded_models의 사유:
not_chat_capable: 모델이 텍스트나 채팅 출력 없이decisions만 광고합니다.unprofiled_model: 일치하는model_profiles항목이 없고smart_routing.unprofiled_models가exclude입니다.
제외 사유¶
검사 순서:
| 사유 | 의미 |
|---|---|
api_key_inactive | 지정한 키가 비활성이거나 만료되었거나 폐기됨 |
model_not_permitted_for_key | 키의 allowed_models에 요청한 모델이 없음 |
model_not_allowed_by_tier | 적용된 허브 정책 티어의 model_allowlist에 요청 모델과 해석된 모델이 모두 없음 |
backend_disabled | 백엔드가 enabled: false |
internal_backend | 백엔드가 internal: true이며 사용자 트래픽을 받지 않음 |
backend_not_permitted_for_key | 키의 allowed_backends에 백엔드가 없음 |
circuit_open | 백엔드 서킷이 쿨다운 중인 open 상태이거나, 프로브 상한에 도달한 half-open 상태 |
capability_mismatch | 모델 메타데이터가 텍스트나 채팅 출력 없이 decisions만 광고하여 채팅 대상이 될 수 없음 |
credential_unavailable | 허브가 관리하는 백엔드의 자격 증명 바인딩이 없거나 아직 복원되지 않음 |
not_in_backend_pool | 카탈로그에는 있지만 라이브 풀에 없음 (예: 리로드 도중) |
unhealthy | 헬스 체커가 백엔드를 비정상으로 판정 |
rate_limited | 백엔드의 rate_limiting.limits.per_backend 버킷이 디스패치 한 번을 감당하지 못함 |
오류¶
알 수 없는 모델이나 hide_variant_models 변형은 코드 model_not_found와 함께 404로 응답합니다. 로컬 키 저장소와 적용된 허브 정책이 모두 모르는 api_key_id는 코드 api_key_not_found와 함께 404로, 비어 있거나 너무 긴 model은 400으로 응답합니다.
다루지 않는 것¶
- 모델 메타데이터를 넘어서는 요청 필드에 따라 달라지는 기능 검사. 예를 들어 해당 기능이 없는 백엔드에 대한 도구 또는 비전 요청입니다.
- 티어 모델 허용 목록을 넘어서는, 일치한 키의 레이트와 예산 적용입니다.
- 전달된
request본문에 대한 스마트 라우팅 분류입니다.
X-Continuum-Route 응답 헤더¶
server.explain_response_header: true(기본값 false, 재시작 필요)를 설정하면 일반 API 응답에 x-continuum-route: <backend>를 추가합니다. 이 헤더는 공유 선택 경로가 마지막으로 고른 백엔드를 담으므로, 재시도와 폴백을 거친 경우 응답을 처리한 시도를 보고합니다. 백엔드 선택에 도달하지 않은 요청에는 헤더가 없습니다.
기본값이 꺼져 있는 이유는 백엔드 이름이 배포 세부 정보이기 때문입니다. 사설 엔진 플릿에서는 용량과 토폴로지를 짐작하게 할 수 있습니다. CORS 계층은 이 헤더를 노출 응답 헤더 목록에 넣으므로, 스위치가 켜져 있으면 브라우저 클라이언트도 읽을 수 있습니다.
백엔드 관리¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET | /admin/backends | 백엔드와 상태 정보 목록 |
POST | /admin/backends | 백엔드 생성 |
GET | /admin/backends/{name} | 백엔드 하나 조회 |
PUT | /admin/backends/{name} | 백엔드 하나 교체 |
DELETE | /admin/backends/{name} | 백엔드 삭제 |
PUT | /admin/backends/{name}/weight | 백엔드 가중치 변경 |
PUT | /admin/backends/{name}/models | 노출 모델 목록 변경 |
POST | /admin/backends/probe | 등록하지 않은 임시 백엔드 후보 검증 및 모델 검색 |
POST | /admin/backends/{name}/models/discover | 선택 모델 목록을 바꾸지 않고 백엔드 하나의 실시간 모델 카탈로그 검색 |
POST | /admin/backends/{name}/check | 상태 머신을 변경하지 않는 즉시 연결 검사 |
GET | /admin/backends/{name}/engine-stats | 백엔드 하나의 최신 엔진 네이티브 통계 스냅샷 조회 |
백엔드 변경은 설정 업데이트 채널로 새 설정 스냅샷을 발행합니다. 모든 필드가 즉시 적용되는 것은 아니므로, /admin/config/hot-reload-status를 확인하고 requires_restart로 표시된 섹션은 재시작하세요.
Admin이 작성한 백엔드 목적지에는 신뢰할 수 있는 시작 파일보다 엄격한 egress 정책을 적용합니다. 백엔드 생성, 교체, 임시 프로브, 설정 섹션 기록, 전체 문서 검증·가져오기·적용, 롤백은 새로 작성하거나 바꾼 백엔드 URL, engine_stats.metrics_url 또는 외부로 요청하는 OAuth 오버라이드(device_code_endpoint, token_poll_endpoint, token_endpoint)가 검증 시점에 루프백, 사설, 링크 로컬 또는 그 밖의 비공개 주소이거나 unix: URL이면 거부합니다. 이 규칙은 전역 engine_stats.allow_external_metrics_url이 다른 공개 메트릭 호스트를 허용하는 경우에도 적용됩니다. 호스트 이름은 비동기 작업자 스레드 밖에서 해석하며 검증 시점에 반환된 모든 주소가 공개 주소여야 합니다. 이는 검증 시점 필터링이며 DNS 주소 고정이 아닙니다. 이름 해석부터 연결까지의 보호가 필요한 배포에서는 네트워크 계층에서도 egress를 제한하세요. 헬스 체크 오버라이드는 같은 원점의 상대 경로여야 하며 accept_status와 warmup_status는 각각 HTTP 상태 코드 16개까지 허용합니다.
정적 설정 파일은 로컬 엔진과 Unix 소켓을 위한 신뢰 경로로 유지됩니다. 파일에서 로드한 백엔드는 사설 목적지를 계속 사용할 수 있고, Admin 기록은 백엔드 URL과 헬스 체크 목적지가 바뀌지 않는 한 가중치나 모델 목록 같은 관련 없는 필드를 바꿀 수 있습니다.
엔진 통계 (GET /admin/backends/{name}/engine-stats)¶
GET /admin/backends/{name}/engine-stats는 지정한 백엔드 이름과 마지막으로 성공한 엔진 네이티브 통계 스냅샷을 반환합니다.
{
"backend": "vllm-01",
"engine_stats": {
"running_requests": 3,
"waiting_requests": 1,
"kv_cache_usage": 0.42,
"generation_throughput_tps": 137.9,
"loaded_models": [],
"source": "vllm_metrics",
"staleness_seconds": 2.4
}
}
engine_stats 객체에는 source(스냅샷을 만든 수집 방식, 예: vllm_metrics, sglang_loads, llamacpp_slots, ollama_ps), staleness_seconds(요청을 처리할 때 계산한 스냅샷 경과 시간), loaded_models(엔진이 나열한 모델이 없으면 [])가 항상 포함됩니다. 그 밖의 엔진별 필드(kv_tokens_used, prefix_cache_hit_rate, context_length, engine_version 등)는 사용할 수 없을 때 0으로 표시하지 않고 생략됩니다. GET /admin/stats/backends도 스냅샷이 있는 백엔드마다 같은 객체를 포함합니다.
엔진 통계 저장소에 해당 이름의 스냅샷이 없으면, 엔드포인트는 빈 본문과 함께 404 Not Found를 반환합니다.
- 해당 백엔드의 엔진 통계가 꺼져 있습니다(전역
engine_stats.enabled, 또는 백엔드 자체의engine_stats.enabled재정의). - 백엔드 유형에 엔진 통계 어댑터가 없습니다.
- 해당 백엔드의 수집이 아직 한 번도 성공하지 않았습니다.
- 해당 이름의 백엔드가 없습니다.
이후 수집이 실패해도 이전에 성공한 스냅샷은 유지되므로, 스냅샷의 경과 시간은 staleness_seconds로 확인하세요.
런타임 백엔드 영속성 (backends_persistence_file)¶
런타임 백엔드 영속성은 선택 기능입니다.
- 영속성 활성화: 루트
backends_persistence_file키를 설정한 뒤 라우터를 재시작하면,POST /admin/backends(WebUI 생성 포함)로 만든 백엔드를 소유자 전용 YAML 사이드카에 저장합니다. 이 키가 없으면 런타임 백엔드는 메모리에만 남고, 재시작하면 사라집니다. - 우선순위 및 Hub 거버넌스: 파일에 선언된 백엔드와 Continuum Hub가 관리하는 백엔드는 사이드카에 기록하지 않습니다. 같은 이름이 충돌하면 파일 선언이 우선하며, 일반 파일 핫 리로드에서는 가려지지 않은 모든 런타임 백엔드를 유지합니다. Hub
overlay모드는 로컬 런타임 백엔드를 보존하지만, 채택된authoritative스냅샷은 사이드카 레코드를 삭제하지 않은 채 유효 풀에서 숨길 수 있습니다. - 핫 리로드 검증: 핫 리로드 시 설정 파일과 런타임 백엔드를 결합해 검증하며, 조합이 유효하지 않으면(예: 리로드된
tracing.headers이름을 이미 런타임 백엔드의request_extensions.headers가 사용하는 경우) 이전에 발행한 설정을 그대로 유지합니다. - 상태 보고 및 파일 보안:
GET /admin/backends, 단일 백엔드 조회, 변경 응답은runtime_persistence.enabled와 각 백엔드의durability값(configured,memory,persistent)을 보고합니다. 시작 시 복원할 수 있도록 사이드카의 자격 증명은 마스킹하지 않으므로, 주 설정 파일과 같은 수준으로 경로를 보호하세요. Unix에서는 모드0600으로 파일을 만들고 원자적으로 교체합니다. 원본 설정 파일에는 되쓰지 않습니다.
실시간 모델 카탈로그 검색 (POST /admin/backends/{name}/models/discover)¶
POST /admin/backends/{name}/models/discover는 읽기 전용입니다. 설정된 백엔드 하나를 정확한 이름으로 찾고, 일반 집계가 쓰는 백엔드별 모델 fetcher를 재사용하여, 설정의 backends[].models 허용 목록을 적용하기 전의 실시간 카탈로그를 반환합니다. 라우팅에 쓰는 선택 모델 목록은 그대로 유지한 채 관리 UI에서 선택 가능한 모델 목록을 채울 때 사용하세요.
curl -X POST http://127.0.0.1:8080/admin/backends/provider-openai/models/discover \
-H "Authorization: Bearer $ADMIN_TOKEN"
{
"backend": "provider-openai",
"object": "list",
"data": [
{
"id": "gpt-5.5",
"object": "model",
"created": 1760000000,
"owned_by": "openai"
}
]
}
- 부작용 없음: 검색 응답은 설정을 기록하거나 집계 모델 캐시를 무효화하지 않으며, 백엔드 헬스와 서킷 브레이커 상태도 바꾸지 않습니다. 응답 본문에는 액세스 토큰, 리프레시 토큰, 토큰 저장소 경로, 원본 인증 헤더가 포함되지 않습니다.
- 지원 백엔드:
- Codex OAuth: 일반 Codex 검색 경로와 동일하게 로드된 OAuth 전략, 토큰 갱신,
chatgpt-account-id처리, 계정 플랜 필터를 사용합니다. - 네이티브 Anthropic:
x-api-key와anthropic-version: 2023-06-01헤더로GET {base}/v1/models를 조회하며,has_more와after_id를 통한 커서 페이지네이션을 지원합니다. - 미지원 백엔드: Codex가 아닌 OAuth 백엔드와 Bedrock은 조회할 공급자 네이티브 카탈로그가 없으므로,
501(error.code: "model_discovery_unsupported")을 반환합니다.
- Codex OAuth: 일반 Codex 검색 경로와 동일하게 로드된 OAuth 전략, 토큰 갱신,
- 오류 코드: 알 수 없는 백엔드 이름은
404(backend_not_found)를 반환합니다. 업스트림 실패는backend_authentication_failed,backend_discovery_timeout,backend_discovery_network_error,backend_discovery_parse_error,backend_discovery_http_error,backend_discovery_response_too_large중 하나로 반환됩니다.
임시 백엔드 후보 프로브 (POST /admin/backends/probe)¶
POST /admin/backends/probe는 백엔드 생성과 동일한 후보 필드와 operations: ["health", "models"]를 받습니다. TCP와 Unix 소켓을 포함한 모든 Admin 전송에서 Admin 인증을 거치며, GET /admin/capabilities는 이 기능을 transient_backend_probe_v1로 광고합니다. 후보는 메모리에서만 검증되며 활성 설정, 설정 기록, 백엔드 풀, 헬스 상태, 서킷 브레이커, 집계 캐시, 환경 파일, 토큰 저장소에 추가되지 않습니다.
응답은 health와 catalog를 분리합니다:
| 필드 | 값 | 동작 |
|---|---|---|
health.credential_status | valid, invalid, unknown, not_required | 인증이 필요 없는 헬스 엔드포인트에 자격 증명을 전달한 경우, 생존 확인만으로는 자격 증명을 검증할 수 없으므로 unknown을 반환합니다. 임시 OAuth 후보는 health.status: "unknown"과 backend_probe_health_unsupported를 반환합니다. |
catalog.source | live, curated, configured, unsupported | 모델 목록 API를 지원하는 후보(OpenAI 호환, Anthropic, Gemini, Ollama, vLLM, llama.cpp, MLX, LM Studio)는 등록된 백엔드 검색과 동일한 URL 구성, 인증 헤더, 응답 크기 제한, 파싱, 정규화, 모델 개수 제한을 사용합니다. Anthropic 실시간 카탈로그 조회가 실패하면 catalog.source는 curated로 폴백하면서 catalog.error에 업스트림 오류를 남기며(401/403에서는 health.credential_status도 갱신), 실시간 조회 없이 명시적 models/model_configs가 주어지면 configured를 반환합니다. Bedrock과 OAuth 후보는 개수 제한이 적용된 configured 항목을 반환하거나, 항목이 없으면 unsupported를 반환합니다. |
프로브 구현은 항상 컴파일되는 src/backend_probe/ 모듈에 있으며, 아웃바운드 Continuum Hub 백엔드 태스크 실행기(backend_tasks_v1)와 공유합니다. 두 경로는 프로세스 전역 동시성·속도 상한을 하나만 공유하며, 지원 백엔드 타입, 엔드포인트 규칙, 응답 크기 제한, 자격 증명 분류 규칙도 동일합니다. Hub 경로는 라우터에 인바운드 엔드포인트를 열지 않고, 결과를 프로토콜의 크기 제한된 타입 구조로 매핑합니다.
기능 광고 (backend_preference_header_v1)¶
GET /admin/capabilities는 capabilities 배열에 transient_backend_probe_v1과 함께 backend_preference_header_v1을 담아 보고합니다.
- 의미: 라우터가 상호 교체 가능한 백엔드 후보 중에서 선택하는 모든 경로에서
x-backend요청 헤더를 지원함을 나타냅니다(x-backend로 백엔드 지정하기 참고). 컴파일 타임 기능이나 현재 설정과 무관하게 포함되며, 개별 엔드포인트 목록은 담지 않습니다. - 클라이언트 활용: Admin 권한이 있는 클라이언트는 버전 번호를 비교하는 대신 이 토큰의 유무로
x-backend지원 여부를 판단할 수 있습니다(구버전 라우터는 헤더를 무시하므로 그냥 보내도 무방합니다). 추론용 API 키만 가진 클라이언트는 추론 응답의x-served-backend헤더로 지원 여부를 확인할 수 있습니다(어느 백엔드가 처리했는지 확인하기 참고).
서킷 브레이커 상태¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET | /admin/circuit/all | 전체 서킷 상태 목록 |
GET | /admin/circuit/{backend}/status | 백엔드 서킷 상태 조회 |
POST | /admin/circuit/{backend}/open | 서킷 강제 열기 |
POST | /admin/circuit/{backend}/close | 서킷 강제 닫기 |
POST | /admin/circuit/{backend}/reset | 서킷 초기화 |
이 엔드포인트는 일반 LLM 프록시 요청이 구동하고 조회하는 것과 동일한 서킷 브레이커 상태 머신을 제어하므로, 일반 프록시 라우팅이 사용하는 상태를 보고하고 조정합니다(예: open은 백엔드가 회복될 때까지 선택에서 제외합니다).
설정 조회¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET | /admin/config | 간단한 설정 요약 |
GET | /admin/config/full | 민감 값을 마스킹한 전체 현재 설정 |
GET | /admin/config/sections | Admin Config API 지원 섹션과 재적용 능력 |
GET | /admin/config/schema | 지원 섹션의 JSON 스키마 |
GET | /admin/config/hot-reload-status | 런타임 핫 리로드 상태와 능력 목록 |
GET | /admin/config/{section} | 민감 값을 마스킹한 지원 섹션 하나 |
GET /admin/config/full은 다음 봉투 구조를 반환합니다.
{
"config": {},
"hot_reload": {
"enabled": true,
"capabilities": {}
},
"metadata": {
"retrieved_at": "2026-07-19T00:00:00Z",
"version": 1
}
}
설정하지 않은 섹션은 config에 명시적 null 값으로 남지 않고 아예 빠집니다. 개별 섹션 조회에서는 GET /admin/config/{section}으로 아직 설정하지 않은 유효한 섹션(예: metrics, fallback)을 조회하면 404가 아니라 200과 "config": null을 반환합니다.
섹션 API가 받는 이름은 다음과 같습니다.
serverbackendshealth_checksloggingretrytimeoutsrate_limitingcircuit_breakerglobal_promptsrequest_paramsadminfallbackmodel_aliasesfilesapi_keysmetricsrouting
이는 관리용 부분집합이며 주 설정 스키마가 받는 모든 최상위 필드 목록이 아닙니다.
설정 검증 및 변경¶
| 메서드 | 경로 | 용도 |
|---|---|---|
PUT | /admin/config/{section} | 지원 섹션 교체 |
PATCH | /admin/config/{section} | 섹션 객체의 재귀 부분 병합 |
POST | /admin/config/validate | 적용 없이 YAML, JSON, TOML 검증 |
POST | /admin/config/export | 기본적으로 마스킹하여 설정 내보내기 |
POST | /admin/config/import | 전체 설정 검증 및 선택적 발행 |
GET | /admin/config/history | 마스킹된 스냅샷을 포함한 최근 기록 최대 50개 조회 |
POST | /admin/config/rollback/{version} | 기록에 있는 설정 스냅샷 발행 |
POST | /admin/config/apply | 전체 설정 후보를 원자적으로 적용하거나, 후보가 없으면 no-op |
PUT과 PATCH의 JSON 본문은 { "config": ... } 래퍼가 아니라 섹션 값 자체입니다.
curl -X PATCH http://127.0.0.1:8080/admin/config/rate_limiting \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"enabled":true}'
성공한 섹션 변경 응답에는 version, requires_restart, hot_reload_capability가 포함됩니다. API는 메모리 내 스냅샷을 발행하며 원본 YAML/TOML 파일에는 쓰지 않습니다.
검증 본문:
{
"content": "server:\n bind_address: 127.0.0.1:8080\nbackends: []\n",
"format": "yaml",
"sections": []
}
sections가 비어 있으면 content는 완전한 설정이어야 합니다. 섹션 단위 검증에서는 content에 해당 최상위 섹션을 넣고 sections에도 그 이름을 나열합니다.
내보내기 본문:
format은 yaml, json, toml 중 하나입니다. sections가 비어 있으면 전체 설정을 내보냅니다. 모든 내보내기는 자격 증명을 마스킹하고 admin.auth를 생략합니다. 기존 include_sensitive: true 필드는 와이어 호환성을 위해 받지만 무시하며 감사 로그에 기록합니다.
format: "toml"은 선택 섹션이 설정되어 있지 않아도 동작합니다. 설정하지 않은 선택 섹션은 직렬화된 설정에서 null로 남지 않고 생략되므로 null 값을 표현하지 못하는 TOML 형식으로도 내보낼 수 있습니다.
가져오기 본문:
{
"content": "server:\n bind_address: 127.0.0.1:8080\nbackends: []\n",
"format": "yaml",
"apply": false,
"dry_run": true
}
가져오기 본문은 1 MiB, 구조 중첩 깊이는 32로 제한됩니다. dry_run: true는 검증만 합니다. apply: true는 전체 후보 설정을 설정 업데이트 채널로 발행하고 기록 버전을 생성합니다. 재시작 전용 섹션은 여전히 재시작이 필요합니다.
내보내기에서 가져오기로 돌아올 때의 마스킹된 비밀 값¶
Admin API가 반환하는 마스킹 값은 어떤 엔드포인트든 똑같이 고정된 센티널 ***CONTINUUM-MASKED:<힌트>***로 감쌉니다. 힌트는 길이만 드러냅니다. 4자를 넘는 값은 앞 두 글자와 길이를 함께 보여주고(sk...(21 chars)), 4자 이하 값은 길이만 보여주며((4 chars)), 불리언이나 숫자를 마스킹하면 ***CONTINUUM-MASKED:hidden***으로 나옵니다. 이 마스킹 규칙은 GET /admin/config/full, GET /admin/config/{section}, GET /admin/backends, GET /admin/config/history 스냅샷에도 똑같이 적용되며 내보내기에서만 쓰는 형식이 아닙니다.
내보내기는 모든 자격 증명을 마스킹합니다. 따라서 편집해서 되돌려 보내는 문서에는 실제 비밀 값 대신 이 자리표시자가 들어 있습니다. 가져오기는 자리표시자를 "지금 적용된 값을 그대로 둔다"는 뜻으로 읽어 실행 중인 비밀 값을 다시 채워 넣고 그대로 둔 경로를 모두 응답에 적습니다.
{
"success": true,
"applied": true,
"version": 4,
"preserved_secret_paths": ["backends[0].api_key"],
"validation": {
"valid": true,
"warnings": [
{
"section": "backends",
"path": "backends[0].api_key",
"message": "'backends[0].api_key' carried a masked placeholder, so the value currently in effect was kept. The submitted document did not change this secret.",
"code": "MASKED_SECRET_PRESERVED"
}
]
}
}
POST /admin/config/validate도 같은 MASKED_SECRET_PRESERVED 경고를 내보냅니다. WebUI의 저장 전 예행 검증이 커밋 전에 어떤 값이 그대로 남는지 보여 준다는 뜻입니다.
설정 문서를 기록하는 모든 엔드포인트가 같은 복원 과정을 거치므로, 예행 검증이 약속한 결과가 실제 저장 결과와 같습니다. POST /admin/config/import, config 후보를 담은 POST /admin/config/apply, PUT/PATCH /admin/config/{section}이 여기에 해당합니다. 섹션 기록에서 자리표시자를 복원하지 못하면 400 Bad Request로 거부하며 본문에는 종전과 같이 success: false와 실패한 경로가 path/code에 담깁니다.
자리표시자는 실행 중인 설정에 그 값을 만들어 낸 원본이 아직 있을 때만 복원됩니다. 배열 항목은 위치가 아니라 id나 name으로 대응시키므로 backends 순서를 바꿔도 제대로 복원되지만 이름을 바꾸면 복원되지 않습니다. guardrails.bypass_api_keys나 rate_limiting.bypass_keys처럼 문자열만 담는 목록은 id도 name도 없어 위치가 유일한 단서입니다. 그래서 항목을 하나 넣거나 빼면 추측하지 않고 거부합니다. 같은 공급자가 발급한 키는 접두사와 길이가 같아서 한 칸 밀린 위치가 옆 키로 그대로 대응해 버리기 때문입니다. 이런 목록을 편집할 때는 실제 값을 직접 넣으세요. 대응할 값을 찾지 못하면 옆에 있던 비밀 값을 엉뚱한 필드에 붙이는 대신 가져오기 전체를 거부하고 실패한 경로를 그대로 알려 줍니다.
| 코드 | 의미 |
|---|---|
MASKED_SECRET_PRESERVED | 경고. 이 경로의 실행 중 값을 그대로 두었습니다. 제출한 문서는 이 값을 바꾸지 않았습니다. |
MASKED_SECRET_UNRESOLVED | 오류. 이 경로의 자리표시자가 실행 중인 설정의 어떤 값과도 대응하지 않습니다. 보통 항목이나 섹션의 이름이 바뀌었거나, 다른 문서에서 옮겨 왔거나, 삭제된 경우, 또는 id나 name이 없는 목록에 항목을 넣거나 뺀 경우입니다. 실제 값을 넣거나, 현재 설정을 다시 내보내 그 문서를 편집하세요. |
LEGACY_MASK_PLACEHOLDER | 오류. 센티널이 생기기 전에 쓰던 자리표시자 형태(***MASKED***, xy...(N chars), ${***VAR***})이며 보통 업그레이드 이전 라우터에서 내보낸 문서에서 나타납니다. 이 형태들은 비밀 값 자체에서 파생되어 실제 자격 증명과 구분할 수 없으므로 복원 근거로 쓰지 않습니다. 현재 설정을 다시 내보내 그 문서를 편집하거나 지적된 경로에 실제 비밀 값을 직접 입력하세요. |
MASKED_SECRET_REPORT_TRUNCATED | 요약하는 목록에 따라 경고 또는 오류로, 최대 한 번만 보고합니다. 자리표시자로 가득 찬 문서가 응답을 부풀리지 못하도록 경로별 목록을 500개로 제한하며 목록에 담기지 않은 채 유지되거나 거부된 경로가 몇 개인지 알려줍니다. |
이미 실행 중인 값과 같은 값은 생김새와 무관하게 그대로 둡니다. 예전 빌드에서 자격 증명이 손상되어 실행 중인 값 자체가 옛 자리표시자 형태인 설정도, 모든 비밀 값을 다시 입력하지 않고 섹션 단위로 계속 편집할 수 있습니다.
자리표시자가 아닌 값은 그대로 반영되므로 자격 증명 교체도 평소대로 됩니다. 자리표시자를 새 비밀 값으로 바꿔 가져오면 됩니다. 환경 변수 참조는 참조인 채로 왕복합니다. ${OPENAI_API_KEY}는 ***CONTINUUM-MASKED:${OPENAI_API_KEY}***로 내보내지고 ${OPENAI_API_KEY}로 복원되며 해석된 값으로 바뀌지 않습니다.
적용 본문:
POST /admin/config/apply는 전체 config 후보를 전달할 때만 실제로 동작합니다. 섹션 편집(PUT/PATCH), import, rollback은 이미 즉시 발행하므로:
config없음(또는null): 명시적 no-op. 아무것도 발행하지 않고 기록 버전도 생성하지 않으며 응답은hot_reload_triggered: false와 빈updated_sections를 반환합니다. 반복 호출해도 빈 기록 버전을 만들지 않습니다.config있음,hot_reload: true: 후보를 검증하고 실행 중인 설정과 비교(diff)한 뒤, 차이가 있으면 설정 업데이트 채널로 원자적으로 발행합니다. 변경된 섹션으로 정확히 하나의 기록 버전을 생성합니다.hot_reload_triggered는 발행이 성공한 뒤에만true이고updated_sections와requires_restart는 실제 diff를 반영합니다. 이 경로에서 후보가 거부되면400 Bad Request를 반환하며 거부 사유는message에 담깁니다.config있음,hot_reload: false: 미리보기 전용. 응답은updated_sections/requires_restartdiff를 반환하지만 아무것도 발행하지 않고 기록 버전도 생성하지 않습니다. 이 모드에서는 후보가 거부되어도200을 유지합니다. 실행이 아니라 어떻게 될지를 물은 요청이기 때문입니다.
실행 중인 설정과 동일한 후보는 no-op으로 처리됩니다.
설정 이력 및 롤백¶
GET /admin/config/history는 마스킹된 config 스냅샷이 포함된 최근 이력 항목을 최대 50개(최신순) 반환하며 POST /admin/config/rollback/{version}은 저장된 스냅샷을 런타임 설정 채널을 통해 다시 적용합니다.
기본적으로 이력은 메모리에만 유지됩니다(admin.max_history_entries 기본값 100, admin.max_history_bytes 기본값 10485760 / 10 MiB). 재시작 후에도 파일 편집 핫 리로드 이력을 유지하려면 admin.history_dir을 설정하세요.
admin:
history_dir: "/var/lib/continuum-router/config-history"
max_history_entries: 100
max_history_bytes: 10485760
- 변수 보간 전 스냅샷(
0600): 파일 편집 핫 리로드가 발생할 때마다 라우터는${VAR}환경 변수 보간 이전의 원시 설정 텍스트를0600파일 권한으로admin.history_dir에 기록하며max_history_entries와max_history_bytes보존 한도를 적용합니다. - Git 관리 설정 파일 관행: 설정 파일은 Git으로 관리하고 모든 시크릿, 관리자 토큰, 우회 키는 파일에 리터럴로 적지 말고
${VAR}환경 변수로 참조하세요. 영속화된 스냅샷은 보간 전 파일 텍스트를 그대로 저장하므로 디스크에는${VAR}자리표시자만 남고 해석된 시크릿은admin.history_dir에 절대 기록되지 않습니다. 롤백(POST /admin/config/rollback/{version}) 시에는 저장된 텍스트를 현재 환경 변수로 다시 파싱·보간하여 기존 롤백 경로로 재적용합니다. - 메모리 전용 Admin API 및 WebUI 편집: Admin API나 WebUI 화면(
PUT/PATCH /admin/config/{section},POST /admin/config/import,POST /admin/config/apply,POST /admin/config/rollback/{version}, 백엔드 변경)을 통해 수행한 변경은 인메모리 이력에만 남으며 디스크에는 기록되지 않습니다.GET /admin/config/history는 영속화된 파일 리로드 항목과 인메모리 항목을 병합해 버전 내림차순으로 반환합니다. - 노드 로컬 범위: 설정 이력은 설계상 노드 로컬(node-local)이며 여러 라우터 노드 간에 공유되지 않습니다.
API 키¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET, POST | /admin/api-keys | 키 목록 또는 생성 |
GET, PUT, DELETE | /admin/api-keys/{id} | 키 조회, 변경 또는 삭제 |
POST | /admin/api-keys/{id}/rotate | 키 값 회전 |
POST | /admin/api-keys/{id}/enable | 키 활성화 |
POST | /admin/api-keys/{id}/disable | 키 비활성화 |
API 키 값은 생성 또는 회전 시에만 반환됩니다. 즉시 안전한 곳에 저장하세요.
prompt_logging_consent(allowed 또는 denied)는 키가 프롬프트 캡처에 대해 나타내는 값을 지정합니다. 생성, 조회, 목록, 수정 응답은 값이 있을 때 이 필드를 반환합니다. 수정 시 동작은 다음과 같습니다.
- 필드를 생략하면 값을 유지합니다.
null을 보내면 값을 지우고, 키는prompt_logging.default_consent를 따릅니다.- 알 수 없는 값은
422 Unprocessable Entity로 응답합니다.
변경 사항은 다음에 입장하는 요청부터 적용됩니다. 런타임 키는 이 값을 저장합니다. 설정 파일이나 키 파일의 키는 저장하지 않으므로 재시작하면 파일의 값으로 돌아갑니다. 계속 유지할 변경은 파일에서 하십시오. 키의 동의가 오는 곳을 참고하세요.
프롬프트 및 통계¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET | /admin/config/prompts | 프롬프트 파일 목록 |
POST | /admin/config/prompts/reload | 프롬프트 파일 다시 읽기 |
GET, PUT | /admin/config/prompts/{path} | 프롬프트 경로 조회 또는 변경 |
GET | /admin/stats | 전체 통계 스냅샷 |
GET | /admin/stats/series | 전체 시계열 |
GET | /admin/stats/models | 모델별 통계 |
GET | /admin/stats/backends | 백엔드별 통계 |
GET | /admin/stats/api-keys | API 키 통계 |
GET | /admin/stats/api-keys/{id} | 키 하나의 통계 |
GET | /admin/stats/api-keys/{id}/models | 키 하나의 모델 통계 |
GET | /admin/stats/api-keys/{id}/series | 키 하나의 시계열 |
GET | /admin/stats/users | 사용자 통계 |
GET | /admin/stats/users/{user_id} | 사용자 하나의 통계 |
GET | /admin/stats/users/{user_id}/models | 사용자 하나의 모델 통계 |
GET | /admin/stats/users/{user_id}/series | 사용자 하나의 시계열 |
POST | /admin/stats/reset | 메모리 통계 초기화 |
GET | /admin/metrics/history | 영속 메트릭 기록. 영속화가 없으면 404 |
GET | /admin/usage/export | 시도 단위 사용량 레코드(원시 또는 그룹, JSON 또는 CSV). admin.stats.enabled와 admin.stats.usage_export.enabled가 모두 켜져 있지 않으면 404 |
GET | /admin/usage/reconciliation | 백엔드별, 윈도별로 원장 출력 토큰을 엔진 생성 카운터와 비교. usage.reconciliation이 없거나 비활성이면 {"enabled": false, "backends": {}} |
GET /admin/stats는 클라이언트 요청 통계와 백엔드 디스패치 통계를 함께 보고합니다:
- 시도 카운터 (
attempts_total,successful_attempts,failed_attempts):overall, 모든models[]항목, 모든backends[]항목에 요청 카운터(total_requests,successful_requests,failed_requests)와 나란히 보고됩니다.attempts_total은 백엔드에 도달한 디스패치(재시도, 폴백 체인 홉, 첫 바이트 이전 스트리밍 홉, 품질 검사 홉, 웹 검색 도구 루프 턴)를 정확히 한 번씩 세고,total_requests는 클라이언트 요청을 셉니다(예:503후 한 번 재시도된 요청은total_requests: 1,attempts_total: 2). 실패한 시도는 연결한 백엔드에 집계되고, 백엔드에 전혀 도달하지 못한 요청은 실패 행을not_dispatched백엔드로 기록합니다. 기간 조회(?window=)는 기간 안에 완료된 요청의 시도를 세며, 해당 필드가 없는 스냅샷은0으로 복원됩니다. 키별·사용자별 통계는 요청 수만 집계합니다. - 용도별 집계 (
by_purpose): 각 purpose(client,guardrail,classifier,action_safety,quality_judgment)를{requests, successful_requests, failed_requests, prompt_tokens, completion_tokens, total_tokens}로 매핑하며,overall과 동일한 기간 필터를 따릅니다.client의requests는 클라이언트 요청 수를, 나머지 내부 purpose는 라우터가 클라이언트 요청을 처리하며 수행한 내부 LLM 호출 수를 셉니다. 내부 호출은overall요청 카운터, 지연 시간 지표,attempts_total에는 합산되지 않지만, 토큰 수는overall,by_outcome,models[]표에 포함됩니다(window=all일 때by_purpose토큰 합계는overall과 일치). 요청과 토큰이 모두 없는 purpose는 생략됩니다. 내부 purpose의 기간별 수치는 최근 내부 호출 25,000건을 담는 별도 링 버퍼에서 집계됩니다(클라이언트 요청은 100,000건 유지).
사용량 내보내기¶
GET /admin/usage/export는 시도 단위 사용량 레코드를 반환하며, 출력 토큰을 실제로 실행한 각 모델에 귀속합니다(스트림 중단 시점까지 생성된 토큰 포함):
- 선행 조건 및 수명 주기:
admin.stats.enabled와admin.stats.usage_export.enabled가 모두 켜져 있어야 하며, 하나라도 꺼져 있으면404(usage_export_disabled)를 반환합니다.admin.stats.enabled만 끄면 기록이 일시 중지되고 기존 레코드가 숨겨지며(다시 켜면 복원),usage_export.enabled를 끄면 버퍼가 비워집니다. 레코드는 라우터별 링 버퍼(usage_export.retention기본값7d,usage_export.max_records기본값1,000,000, 오래된 항목부터 제거)에 보관되며,admin.stats.persistence가 켜져 있으면 통계 스냅샷에 함께 저장됩니다. 다중 라우터 환경에서는 각 라우터의 내보내기를 합산하거나 Continuum Hub를 사용하세요. - 레코드 생성 기준: 백엔드에 도달한 시도마다(재시도, 폴백 홉, 웹 검색 턴 각각 1건) 레코드 1건이 생성되며, 로컬 응답 캐시 재생마다 1건이 추가됩니다. 백엔드에 도달하지 못한 요청은 레코드가 생성되지 않습니다.
- 레코드 스키마 (필드 순서):
completed_at_ms,request_id,attempt_seq,parent_attempt,key_id(로컬 해시api_key_id또는 키가 없으면anonymous, 평문 키는 절대 기록하지 않음),user_id,team_id(항상null),model,requested_model(control-plane빌드에서 허브 라우팅 규칙이 모델을 바꾼 경우에만 설정),backend,backend_id(스트림 종료 시 백엔드 이름으로 조회하며, 스트림 도중 이름이 바뀌면null),purpose,outcome,token_source,estimated,stream,http_status,input_tokens,cached_input_tokens,output_tokens,reasoning_tokens,latency_ms,cache_hit_type.- 캐시 재생 레코드:
backend: "local-cache",output_tokens: 0, 모든 프롬프트 토큰을cached_input_tokens로,token_source: "none",cache_hit_type을exact,prefix,semantic중 하나로 기록합니다. - 모델 이름 길이 제한:
model과requested_model은 UTF-8 문자 경계에 맞춰 최대 256바이트로 잘립니다.
- 캐시 재생 레코드:
| 파라미터 | 의미 |
|---|---|
from, to | Unix 밀리초 또는 RFC 3339로 나타낸 범위 [from, to). to 기본값은 현재, from 기본값은 to에서 보관 기간을 뺀 값. 잘못된 값이나 from >= to는 400 invalid_range. |
group_by | period:1h, period:1d, model, backend_id, outcome, purpose, key_id의 쉼표 목록. period: 토큰은 하나까지, 각 키는 한 번만. 없거나 비어 있으면 원시 레코드. 그 밖의 값(끝의 쉼표 포함)은 400 invalid_group_by. |
format | json(기본값) 또는 csv. 그 밖의 값은 400 invalid_format. |
outcome, purpose, model, backend_id | 필터. 각각 정확히 일치하는 쉼표 목록이며 파라미터끼리는 AND, 한 파라미터 안에서는 OR. |
limit | 원시 모드 전용: 1에서 100,000, 기본값 10,000. 범위 밖, 숫자가 아닌 값, group_by와 함께 쓰면 400 invalid_limit. |
cursor | 원시 모드 전용: 이전 페이지의 next. 이 내보내기가 발급하지 않은 값이나 group_by와 함께 쓰면 400 invalid_cursor. |
share_of | JSON 전용: 출력 토큰 점유율을 계산할 모델의 쉼표 목록. format=csv와 함께 쓰면 400 share_requires_json. |
해석할 수 없는 쿼리 문자열은 400 invalid_query를 반환합니다. 모든 거부 응답 본문은 {"error":{"code":"...","message":"..."}} 형식입니다.
- 원시 모드 정렬 및 커서: 레코드를
(completed_at_ms, request_id, attempt_seq)순으로 정렬한 뒤, 링 버퍼가 도착 순서대로 부여하는 내부 순번으로 정렬해 최대limit개를 반환합니다. 내부 순번 덕분에 요청 범위 밖에서 생성된 레코드(빈request_id와 시도 1 공유)나 응답마다 같은request_id를 공유하는 실시간 세션 사이에서도 완전한 정렬 순서가 보장되며, 영속화가 켜져 있으면 재시작 후에도 커서가 유지됩니다.cursor는 마지막 반환 레코드의{completed_at_ms}:{request_id}:{attempt_seq}:{sequence}를 URL 안전 base64로 인코딩한 값입니다. JSON 응답은{"from": <rfc3339>, "to": <rfc3339>, "group_by": [], "records": [...], "next": <커서 또는 null>}봉투를 쓰며, CSV 응답은X-Next-Cursor헤더로 커서를 전달합니다(마지막 페이지에서는 생략). - 그룹 모드: 고유 키 조합마다 한 행을 키 조합 순으로 정렬해 반환합니다. 각 행은
group_by순서의 키(period_start는 시간 또는 UTC 일 단위로 내림한 RFC 3339 UTC 값이며, 값이 없으면 JSONnull·CSV 빈 필드) 뒤에 7개 측정값을 포함합니다:requests(고유request_id수, 요청 범위 밖 레코드는 각각 1건으로 집계),attempts(레코드 수),input_tokens,cached_input_tokens,output_tokens,reasoning_tokens,estimated_output_tokens(estimated: true인 레코드의 출력 토큰 합). JSON 응답은{"from", "to", "group_by": [...], "rows": [...]}봉투를 사용합니다. - CSV 형식 및 수식 인젝션 방지: 응답은
text/csv; charset=utf-8및Content-Disposition: attachment; filename="usage-export-<from>-<to>.csv"(타임스탬프는YYYYMMDDTHHMMSSZ) 헤더와 함께 헤더 행 1개, CRLF 줄바꿈, BOM 없는 UTF-8, 최소한의 RFC 4180 따옴표 규칙을 사용합니다.=,+,-,@, 탭, CR, 또는'로 시작하는 필드는 스프레드시트가 수식으로 실행하지 않도록 따옴표 처리 전에 앞에'를 하나 더 붙입니다. 원본 문자열이 필요한 파서는'로 시작하는 필드에서 앞의'하나를 제거하면 됩니다. JSON 출력은 변경되지 않습니다. - 모델 점유율 (
share_of): JSON 응답에"share": {"models": [...], "numerator_output_tokens": N, "denominator_output_tokens": D, "share": N / D}를 추가합니다(D == 0이면null). 두 합계 모두 캐시 적중(cache_hit_type설정, 규칙 R7)을 제외합니다. 필터가 없으면 가드레일·분류기 호출을 포함한 모든outcome(규칙 R9)과 모든purpose(규칙 R8)를 포함하며, 명시적 필터(outcome,purpose,model,backend_id)를 지정하면 분자와 분모 모두 동일하게 좁혀집니다.
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://127.0.0.1:8080/admin/usage/export?group_by=period:1d,model,backend_id,outcome&format=csv" \
-o usage.csv
사용량 대조¶
GET /admin/usage/reconciliation은 usage.reconciliation 설정에 따라 백엔드별로 사용량 원장에 기록된 출력 토큰과 엔진 자체의 생성 카운터를 윈도 단위로 비교합니다. 설정이 없거나 비활성이면 {"enabled": false, "backends": {}}와 함께 200으로 응답하며, 모델 이름, 키 ID, 프롬프트/응답 텍스트는 절대 포함하지 않습니다.
| 필드 | 설명 |
|---|---|
windows | 백엔드당 최대 history개까지 최신순으로 정렬된 윈도 목록입니다. 각 윈도의 result는 within_tolerance, drift, no_engine_data(새 엔진 스크레이프 값이 없거나 기준값만 잡는 첫 샘플), reset(엔진 재시작 감지) 중 하나입니다. |
engine_generation_tokens | 해당 윈도의 엔진 카운터 증가분이며, result가 no_engine_data일 때는 null입니다. |
drift_ratio | (engine - ledger) / max(engine, 1)(원장이 적게 집계하면 양수)이며, result가 within_tolerance 또는 drift일 때만 값이 설정됩니다. |
totals | 대조기 시작 또는 마지막 백엔드 초기화 이후 엔진 값과 비교 가능한 윈도(within_tolerance, drift, reset)의 누적 합계입니다. 윈도 경계에 걸친 진행 중 요청과 reset 윈도(엔진 재시작 직전 스크레이프부터 재시작까지의 생성분이 원장 쪽에만 집계됨) 때문에 근사치입니다(엔진 카운터와의 대조 참고). |
dropped_samples | 대조기 큐가 가득 차 버려진 원장 샘플 수입니다(0보다 크면 원장 쪽이 적게 집계됨을 뜻함). |
{
"enabled": true,
"window": "5m",
"max_drift": 0.05,
"history": 24,
"dropped_samples": 0,
"backends": {
"vllm-a": {
"totals": {"ledger_output_tokens": 48211, "engine_generation_tokens": 48230, "drift_ratio": 0.0004},
"windows": [
{"backend": "vllm-a", "window_start_ms": 1791700000000, "window_end_ms": 1791700300000,
"ledger_output_tokens": 9120, "engine_generation_tokens": 9124, "drift_ratio": 0.0004,
"result": "within_tolerance"}
]
}
}
}
라우팅, 캐시, 가드레일 제어¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET | /admin/prefix-routing/stats | 접두사 라우팅 통계 |
GET | /admin/provider-failures | 백엔드별 최근 프로바이더 실패(아래 참고) |
GET | /admin/response-cache/stats | 응답 캐시 통계 |
POST | /admin/response-cache/invalidate | 응답 캐시 항목 무효화 |
GET | /admin/kv-index/stats | KV 인덱스 통계 |
GET | /admin/kv-index/backends | 백엔드별 KV 인덱스 상태 |
POST | /admin/kv-index/clear | KV 인덱스 비우기 |
GET | /admin/gemini-context-cache/stats | Gemini 컨텍스트 캐시 통계 |
POST | /admin/gemini-context-cache/clear | Gemini 컨텍스트 캐시 상태 비우기 |
GET, PUT | /admin/smart-routing/model-profiles | 모델 프로필 목록 또는 교체 |
GET | /admin/smart-routing/model-profiles/{model} | 모델 프로필 하나 조회 |
GET | /admin/smart-routing/status | 스마트 라우팅 상태 |
GET | /admin/smart-routing/stats | 스마트 라우팅 통계 |
POST | /admin/smart-routing/classify | 확인용 요청 분류 |
POST | /admin/smart-routing/simulate | 라우팅 시뮬레이션 |
GET, PUT | /admin/smart-routing/policies | 정책 조회 또는 교체 |
GET | /admin/smart-routing/load-state | 백엔드 부하 상태 |
GET | /admin/smart-routing/cache/stats | 분류기 캐시 통계 |
POST | /admin/smart-routing/cache/clear | 분류기 캐시 비우기 |
GET, PATCH | /admin/guardrails | 가드레일 정책 조회 또는 부분 변경 |
PUT | /admin/guardrails/providers/{name} | 가드레일 공급자 변경 |
PUT, DELETE | /admin/guardrails/routes/{route} | 경로 오버라이드 설정 또는 제거 |
POST | /admin/guardrails/test | 설정된 가드레일로 텍스트 시험 실행 |
GET /admin/provider-failures는 백엔드마다 최근에 반환한 오류 응답을 보여 주므로, 요청 트레이스를 맞춰 보지 않고도 실패하는 폴백 체인을 디버깅할 수 있습니다. HTTP 오류 상태로 응답한 백엔드 홉은 어느 경로, 재시도, 폴백 홉에서 보냈든 모두 기록됩니다. 기록에는 상태 코드, 프로바이더 오류 메시지를 마스킹한 요약(저장 전에 자격 증명과 내부 엔드포인트를 제거하고 200자에서 자르며, 본문을 읽을 수 없으면 빈 문자열), Unix 밀리초 단위 시각이 담깁니다. 백엔드마다 최근 실패 20개를 최신순으로 보관하고 추적하는 백엔드는 최대 256개입니다. 풀에서 제거된 백엔드는 목록에서도 빠집니다. 데이터는 프로세스 로컬이며, 재시작하면 초기화됩니다.
{
"backends": {
"provider-openai": [
{"status": 429, "summary": "Rate limit reached for gpt-5.6-terra", "at_ms": 1761700000123},
{"status": 401, "summary": "Incorrect API key provided: [redacted]", "at_ms": 1761699990456}
]
}
}
메트릭의 provider_failures_total{backend, status} 카운터가 같은 이벤트를 셉니다.
스마트 라우팅 상태와 진단¶
GET /admin/smart-routing/status는 라우터가 실제로 실행하는 스마트 라우팅 상태를 보고합니다. Hub 거버넌스 아래에서는 로컬 smart_routing: 섹션과 Hub 정책을 합성한 결과이므로 최상위 필드가 로컬 파일과 다를 수 있으며 로컬 값은 configured 아래에 보고됩니다. 스마트 라우팅이 동작하지 않는 라우터는 enabled: false와 message를 반환하며 source, hub_governed, configured 필드도 함께 포함합니다.
| 필드 | 내용 |
|---|---|
enabled | 실행 중인 스마트 라우터의 활성 여부. 적용 시 거부된 섹션은 configured.enabled가 true여도 여기서는 false입니다 |
source | Hub 스마트 라우팅 정책을 실행 중이면 hub, 그 밖에는 local(Hub 없음, Hub 해제, 거부된 Hub 정책) |
hub_governed | 이 라우터에서 control-plane 조정기가 스마트 라우팅을 관리하는지 여부 |
smart_routing_policy_digest | 실행 중인 Hub 정책 다이제스트로, Hub에 확인 응답한 값과 같습니다. source가 local이면 null |
declined_hub_policy | 라우터가 거부한 Hub 정책의 smart_routing_policy_digest와 error_code. 없으면 null |
virtual_model, intercept_all, classifier_method, policy_count | 실행 중인 라우터가 사용하는 실효 값 |
default_tier, profile_count | 실행 중인 티어 레지스트리의 값. profile_count는 정확히 일치하는 프로필과 자동 추론 프로필을 셉니다 |
has_llm_classifier, has_system_one_classifier, system_one_shadow | 실행 중인 분류기 상태 |
load_state, load_monitoring_enabled, debug_headers | 부하 모니터 상태와 디버그 헤더 스위치 |
configured | 로컬 섹션 값: enabled, virtual_model, intercept_all, default_tier, classifier_method, system_one_shadow, load_monitoring_enabled, policy_count, profile_count(설정된 model_profiles 항목 수), unprofiled_models |
unprofiled_models, candidate_models, excluded_unprofiled_models, excluded_incompatible_models | 실행 중인 smart_routing.unprofiled_models 값, 스마트 라우팅이 고를 수 있는 정상 사용자 가시 모델(각 항목의 model, tier, 프로필 source), exclude에서 프로필이 없어 제외된 모델, 메타데이터가 text나 chat 출력 없이 decisions만 광고해 제외된 모델. 내부 또는 비활성 백엔드만 제공하는 모델과 hide_variant_models: true 실험이 예약한 변형 모델은 나타나지 않습니다 |
POST /admin/smart-routing/classify와 POST /admin/smart-routing/simulate는 {"payload": <chat completion 본문>}을 받으며 simulate는 신원 조건을 평가하기 위한 key_tier, org, headers도 받습니다. classify는 분류 객체를 반환하고 simulate는 같은 직렬화기로 만든 같은 객체를 classification 아래에 반환하므로, when.language나 when.requires 정책을 simulate만으로 확인할 수 있습니다.
| 분류 필드 | 내용 |
|---|---|
complexity, domain, confidence, classifier_type | 분류 결과와 이를 만든 분류기 |
language | 사용자 텍스트의 짧은 BCP-47 태그. 문자가 없으면 null |
required_capabilities | 요청에 필요한 기능(vision, function_calling 등) |
reasoning | 분류기가 제공한 설명 |
signals | 감지된 각 신호의 name, strength, influences |
simulate는 이 밖에 routed, rejected, target_model, policy(name, tier, prefer_domains, require_capabilities), load_state, input_tokens(when.input_tokens 정책과 비교한 추정치이며 이 조건을 읽는 정책이 없으면 null), classification_duration_ms, available_models를 반환하며 거부되었거나 라우팅할 수 없는 요청은 분류 대신 reason을 담습니다.
파일, ACP, 컨트롤 플레인¶
| 메서드 | 경로 | 용도 |
|---|---|---|
GET, POST | /admin/files | 파일 목록 또는 업로드. Files 비활성 시 404 |
GET, DELETE | /admin/files/{file_id} | 메타데이터 조회 또는 파일 삭제 |
GET | /admin/files/{file_id}/content | 파일 내용 다운로드 |
GET | /admin/acp/status | ACP 서비스 상태 |
GET | /admin/acp/sessions | 활성 ACP 세션 |
GET | /admin/acp/agent.json | ACP 에이전트 설명자 |
GET | /admin/control-plane/status | 컨트롤 플레인 에이전트 상태. control-plane 빌드에만 경로 존재 |
GET | /admin/control-plane/policy | 라우터가 실행 중인 Hub 정책. 키 해시와 비밀값은 제외. control-plane 빌드에만 경로 존재 |
POST | /admin/control-plane/enroll | 권한이 필요한 라우터 측 Hub 등록. control-plane 빌드에만 경로 존재 |
Note:
POST /admin/files로 업로드한 파일은 소유자 없이 저장되지 않고 합성 관리자 소유자(__admin__)로 귀속됩니다.files.auth.enforce_ownership가 켜져 있으면 테넌트 키는 공개 Files API에서 관리자 업로드 파일을 읽거나 삭제할 수 없고,admin스코프 키는admin_can_access_all을 통해 접근합니다. API 요청에서 파일을 참조할 때도 같은 정책이 적용됩니다.
Hub 정책 조회¶
GET /admin/control-plane/policy는 라우터가 실행 중인 Hub 정책을 보고하며 다른 엔드포인트와 같은 Admin 인증을 사용합니다. 에이전트가 비활성이면 {"enabled": false}를, control_plane.policy.enabled가 꺼져 있으면 {"enabled": true, "policy_sync_enabled": false}를 반환합니다. 그 밖에는 synced가 엔벨로프 적용 여부를 나타내며 한 번도 등록하지 않은 라우터는 빈 목록과 함께 synced: false를 보고합니다.
| 필드 | 내용 |
|---|---|
revision, issued_at_ms, last_sync_ms | 적용된 엔벨로프의 커서, Hub가 발행한 시각, 이 라우터가 적용한 시각 |
status | 라우터가 Hub에 보고하는 capability와 활성·거부 리비전 식별자. status.rejected는 거부한 리비전을 오류 코드와 함께 나타냄 |
tiers | Hub가 지정한 티어별 제한과 key_count, revoked_key_count |
keys | total, revoked, unresolved_tier(엔벨로프에 정의되지 않은 티어를 가리키는 키) |
substitution_rules, equivalence_classes, routing_windows | 실제 적용 중인 집합. 엔벨로프에 해당 집합이 없으면 이전 집합을 유지 |
optimization | 조직 최적화 정책. 첫 동기화 전이거나 적용된 엔벨로프에 최적화 블록이 없으면 null(이때 라우터는 기본값으로 동작). 블록 안의 semantic_similarity_bps와 cache_ttl_secs는 허브가 명시하지 않았으면 생략되며 이때 라우터는 기본값을 씁니다 |
model_budgets | 적용 중인 모델 예산 단위와 풀별 tracked_keys, exhausted_keys, unknown_keys. 강제 적용하지 않으면 null |
restored | 옵트인 control_plane.policy.persist_key_table 사이드카에서 복원한 키 테이블이 동기화된 엔벨로프 대신 서빙되는 동안 설정됨: revision, issued_at_ms, synced_at_ms(유효 기한 기준 시각), restored_at_ms, expires_at_ms, key_count. 이 동안 synced는 false이고 tiers와 keys만 채워짐. 그 밖에는 null |
키 해시와 프로바이더 자격 증명은 포함하지 않으며 키는 개수로만 나타납니다. Hub에 연결할 수 없는 동안에도 라우터는 마지막으로 적용한 정책을 계속 강제 적용하므로, 현재 정책인지 마지막으로 알려진 정책인지는 GET /admin/control-plane/status의 heartbeat와 함께 보고 판단합니다. GET /admin/control-plane/status는 policy_sync.source(synced, restored, none), policy_sync.restored 아래 같은 restored 블록, 그리고 영속화가 켜져 있으면 last_persisted_ms, write_failures, max_age_secs를 담은 policy_sync.key_table_persistence도 보고합니다.
라우터에서 시작하는 Hub 등록¶
POST /admin/control-plane/enroll은 다른 모든 Admin 엔드포인트와 같은 Admin 인증 및 감사 미들웨어를 사용합니다. JSON 본문에는 필수 token, 선택적 hub_url과 router_name, 선택적 불리언 replace를 전달합니다.
{"token":"single-use-token","hub_url":"https://hub.example.com","router_name":"edge-a","replace":false}
라우터는 HTTPS(개발용 루프백 HTTP는 허용)로 토큰을 교환하고 기존 소유자 전용 자격 증명 저장소를 통해 Hub가 발급한 tenant_id, router_id, router_credential만 저장한 뒤 state: "pending_restart"를 반환합니다. 등록 토큰은 저장, 응답, 로그에 절대 남지 않으며 WebUI는 성공과 실패 모두에서 입력값을 지웁니다. 새 신원을 활성화하려면 라우터를 재시작합니다.
정상적인 영속 자격 증명이 이미 있으면 replace: true를 명시하지 않는 한 409 ALREADY_ENROLLED를 반환합니다. 실행 중인 에이전트가 거부됨을 관측한 자격 증명은 GET /admin/control-plane/status에서 rejected로 보고되며 상태 파일을 삭제하거나 replace를 지정하지 않고도 교체할 수 있습니다. 이 엔드포인트는 시작 시 등록과 교환을 직렬화하므로 동시 시도가 서로 덮어쓰지 않습니다.
control_plane 섹션은 시작 시 소유됩니다. GET /admin/config/control_plane으로 계속 읽을 수 있지만 GET /admin/config/sections의 편집 가능 목록에서는 제외되며 PUT/PATCH /admin/config/control_plane은 안내와 함께 409 STARTUP_OWNED_SECTION을 반환합니다. enabled, 정책, 주기, 상태 파일 등 에이전트 설정은 소스 YAML/TOML을 편집하고 재시작하십시오. 기존 설정 파일 토큰 등록 흐름도 계속 지원됩니다.
거부된 설정 변경¶
제출한 값 때문에 거부된 요청이 실행 중 상태를 바꾸려던 요청이었다면 400 Bad Request를 반환합니다. 변경이 아니라 판정을 요청한 경우에는 200을 유지하고 결과를 본문에 담습니다. 기준은 엔드포인트가 아니라 요청의 성격입니다.
| 요청 | 거부 시 상태 코드 |
|---|---|
PUT/PATCH /admin/config/{section} | 400 |
apply: true이고 dry_run: false인 POST /admin/config/import | 400 |
dry_run: true이거나 apply: false인 POST /admin/config/import | 200, 결과는 success와 validation에 |
config 후보가 있고 hot_reload: true인 POST /admin/config/apply | 400 |
후보가 없거나, 후보가 실행 중 설정과 같거나, hot_reload: false인 POST /admin/config/apply | 200, 결과는 success와 message에 |
POST /admin/config/validate | 파싱 가능한 입력이면 언제나 200, 결과는 valid와 errors에 |
| 라우터에 핫 리로드 채널이 없을 때 위의 변경 요청 | 503, 본문은 그대로("success": false) |
응답 본문에는 "success": false와 설명이 담긴 error/validation.errors가 포함되며 400 상태 코드를 반환하므로 curl -f와 대부분의 HTTP 클라이언트 라이브러리처럼 상태 코드를 기준으로 삼는 자동화가 거부된 설정 변경을 정상 적용으로 잘못 읽지 않습니다.
라우터에 핫 리로드 채널이 없으면 실행 중 상태를 바꾸려는 요청은 "success": false와 'Hot reload not available' 메시지를 담아 503 Service Unavailable을 반환합니다. 요청 자체는 유효하고 없는 것은 서버 기능이기 때문입니다. 대상은 PUT/PATCH /admin/config/{section}, apply: true이고 dry_run: false인 POST /admin/config/import, 변경된 후보와 hot_reload: true를 보낸 POST /admin/config/apply, POST /admin/config/rollback/{version}(이 경우 이력 버전을 기록하지 않음), 그리고 백엔드 변경(POST /admin/backends, PUT/DELETE /admin/backends/{name}, weight와 models 하위 경로)입니다. 미리보기와 예행 실행은 아무것도 적용하지 않으므로 200을 유지합니다. 가져오기의 크기와 중첩 깊이 제한은 각각 SIZE_LIMIT_EXCEEDED, NESTING_LIMIT_EXCEEDED 코드를 따로 사용합니다.
상태 코드¶
200: 요청이 처리됨. 판정 엔드포인트는 결과를 여기에 담으므로POST /admin/config/validate,POST /admin/config/import예행 실행,POST /admin/config/apply미리보기 응답에는"valid": false나"success": false가 있을 수 있습니다.400: 잘못된 요청, 또는 제출된 값 때문에 라우터가 거부한 설정 변경(본문에"success": false포함).401: Bearer/Basic 자격 증명 누락 또는 불일치.403: 허용되지 않은 IP 또는 권한 부족.404: 없는 리소스, 런타임 기능 비활성, 또는 컴파일 기능이 없어 경로가 없음.409: 기존 자격 증명 교체 동의 필요, 라우터 이름 충돌, 또는 시작 시 소유되는 런타임 섹션.422: Hub가 제공된 등록 토큰을 거부함.413: 업로드가 경로 본문 제한을 초과함.502: 아웃바운드 Hub 등록 교환 실패.500: 내부 실패 또는 인증 방식에 필요한 런타임 상태가 없음.503: 핫 리로드를 사용할 수 없어 적용할 수 없는 설정 또는 백엔드 변경(본문을 반환하는 엔드포인트는"success": false포함).