Admin REST API 레퍼런스¶
이 문서는 Continuum Router의 Admin REST API로 설정 제어 애플리케이션을 만드는 개발자를 위한 가이드입니다. 설정 관리 API를 사용하면 서버 재시작 없이 런타임에 설정을 보고, 수정하고, 관리할 수 있습니다.
목차¶
- 개요
- 인증
- 기본 URL 및 헤더
- 설정 쿼리 API
- 설정 수정 API
- 설정 저장/복원 API
- 백엔드 관리 API
- API 키 관리 API
- 통계 API
- 지속 메트릭 로그 API
- 응답 캐시 Admin API
- KV 캐시 인덱스 Admin API
- 스마트 라우팅 Admin API
- 가드레일 Admin API
- 데이터 모델
- 핫 리로드 동작
- 오류 처리
- 클라이언트 SDK 예제
- 모범 사례
- 보안 고려 사항
개요¶
Admin REST API는 Continuum Router의 설정 시스템에 프로그래밍 방식으로 액세스할 수 있게 하며 다음을 지원합니다:
- 실시간 설정 보기: 민감한 데이터가 자동으로 마스킹된 현재 설정 조회
- 동적 설정 업데이트: 서버 재시작 없이 설정 섹션 수정
- 설정 버전 관리: 전체 히스토리 및 롤백 기능으로 변경 사항 추적
- 백엔드 관리: 백엔드를 동적으로 추가, 제거, 수정
- 내보내기/가져오기: 여러 형식 (YAML, JSON, TOML)으로 설정 저장 및 복원
주요 기능¶
| 기능 | 설명 |
|---|---|
| 핫 리로드 | 섹션 타입에 따라 변경 사항이 즉시 또는 점진적으로 적용됨 |
| 민감 정보 마스킹 | API 키, 비밀번호, 토큰이 응답에서 자동으로 마스킹됨 |
| 검증 | dry-run 지원으로 적용 전 모든 변경 사항 검증 |
| 감사 로깅 | 보안 및 규정 준수를 위해 모든 수정 사항 로깅 |
| 히스토리 추적 | 롤백을 위해 최대 100개의 설정 버전 유지 |
인증¶
모든 Admin API 엔드포인트는 Admin Auth 시스템을 통한 인증이 필요합니다.
인증 방법¶
1. Bearer 토큰¶
2. Basic 인증¶
3. API 키 헤더¶
설정¶
config.yaml에서 admin 인증을 설정합니다:
admin:
auth:
method: bearer_token # 옵션: none, bearer_token, basic, api_key
token: "${ADMIN_TOKEN}" # 환경 변수 지원
# Basic auth의 경우:
# username: admin
# password: "${ADMIN_PASSWORD}"
# IP 화이트리스트 (선택 사항)
ip_whitelist:
- "127.0.0.1"
- "10.0.0.0/8"
# 설정 가능한 제한
max_history_entries: 100
max_backend_name_length: 256
기본 URL 및 헤더¶
기본 URL¶
일반 요청 헤더¶
일반 응답 헤더¶
설정 쿼리 API¶
전체 설정 가져오기¶
민감한 정보가 마스킹된 전체 설정을 조회합니다.
응답¶
{
"config": {
"server": {
"bind_address": "0.0.0.0:8080",
"workers": 4
},
"backends": [
{
"name": "openai",
"url": "https://api.openai.com",
"api_key": "sk-***abcd",
"weight": 1
}
],
"logging": {
"level": "info"
},
"rate_limiting": {
"enabled": true,
"requests_per_minute": 100
}
},
"hot_reload_enabled": true,
"last_modified": "2025-12-13T10:30:00Z"
}
예제¶
설정 섹션 목록¶
핫 리로드 기능과 함께 사용 가능한 모든 설정 섹션을 가져옵니다.
응답¶
{
"sections": [
{
"name": "server",
"description": "Server configuration including bind address and workers",
"hot_reload_capability": "requires_restart"
},
{
"name": "backends",
"description": "Backend server configurations",
"hot_reload_capability": "gradual"
},
{
"name": "logging",
"description": "Logging configuration",
"hot_reload_capability": "immediate"
},
{
"name": "rate_limiting",
"description": "Rate limiting configuration",
"hot_reload_capability": "immediate"
},
{
"name": "circuit_breaker",
"description": "Circuit breaker configuration",
"hot_reload_capability": "immediate"
},
{
"name": "retry",
"description": "Retry policy configuration",
"hot_reload_capability": "immediate"
},
{
"name": "timeouts",
"description": "Timeout configuration",
"hot_reload_capability": "gradual"
},
{
"name": "health_checks",
"description": "Health check configuration",
"hot_reload_capability": "gradual"
},
{
"name": "global_prompts",
"description": "Global prompt injection configuration",
"hot_reload_capability": "immediate"
},
{
"name": "fallback",
"description": "Model fallback configuration",
"hot_reload_capability": "gradual"
},
{
"name": "files",
"description": "Files API configuration",
"hot_reload_capability": "gradual"
},
{
"name": "api_keys",
"description": "API keys configuration",
"hot_reload_capability": "immediate"
},
{
"name": "metrics",
"description": "Metrics and monitoring configuration",
"hot_reload_capability": "gradual"
},
{
"name": "admin",
"description": "Admin API configuration",
"hot_reload_capability": "gradual"
},
{
"name": "routing",
"description": "Request routing configuration",
"hot_reload_capability": "gradual"
}
]
}
예제¶
curl -s http://localhost:8080/admin/config/sections \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.sections[].name'
섹션 설정 가져오기¶
특정 섹션의 설정을 조회합니다.
경로 파라미터¶
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
section |
string | 예 | 섹션 이름 (위 목록 참조) |
응답¶
{
"section": "logging",
"config": {
"level": "info",
"format": "json",
"file": "/var/log/continuum-router.log"
},
"hot_reload_capability": "immediate",
"description": "Logging configuration"
}
예제¶
# 로깅 설정 가져오기
curl -s http://localhost:8080/admin/config/logging \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
# 백엔드 설정 가져오기
curl -s http://localhost:8080/admin/config/backends \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
설정 스키마 가져오기¶
설정 검증을 위한 JSON Schema를 조회합니다.
쿼리 파라미터¶
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
section |
string | 아니오 | 특정 섹션에 대한 스키마만 가져오기 |
응답¶
{
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"server": {
"type": "object",
"properties": {
"bind_address": {
"type": "string",
"pattern": "^[^:]+:[0-9]+$",
"description": "Server bind address in host:port format"
},
"workers": {
"type": "integer",
"minimum": 1,
"description": "Number of worker threads"
}
}
},
"logging": {
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": ["trace", "debug", "info", "warn", "error"]
}
}
}
}
}
}
예제¶
# 전체 스키마 가져오기
curl -s http://localhost:8080/admin/config/schema \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
# 특정 섹션 스키마 가져오기
curl -s "http://localhost:8080/admin/config/schema?section=logging" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
설정 수정 API¶
섹션 설정 대체¶
전체 섹션 설정을 새 값으로 대체합니다.
요청 본문¶
응답¶
{
"success": true,
"message": "Configuration updated successfully",
"version": 5,
"hot_reload_capability": "immediate",
"applied": true,
"warnings": []
}
예제¶
# 로깅 레벨을 debug로 업데이트
curl -X PUT http://localhost:8080/admin/config/logging \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config": {
"level": "debug"
}
}'
섹션 부분 업데이트¶
JSON 병합 패치 의미론을 사용하여 부분 업데이트를 적용합니다.
요청 본문¶
지정된 필드만 업데이트되고 다른 필드는 변경되지 않습니다.
응답¶
{
"success": true,
"message": "Configuration partially updated",
"version": 6,
"hot_reload_capability": "immediate",
"applied": true,
"merged_config": {
"level": "warn",
"format": "json",
"file": "/var/log/continuum-router.log"
}
}
예제¶
# 속도 제한 값만 업데이트
curl -X PATCH http://localhost:8080/admin/config/rate_limiting \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config": {
"requests_per_minute": 200
}
}'
설정 검증¶
변경 사항을 적용하지 않고 설정을 검증합니다.
요청 본문¶
{
"section": "server",
"config": {
"bind_address": "0.0.0.0:9090",
"workers": 8
},
"dry_run": true
}
응답 (유효)¶
{
"valid": true,
"errors": [],
"warnings": [
{
"field": "bind_address",
"message": "Changing bind_address requires server restart"
}
],
"hot_reload_capability": "requires_restart"
}
응답 (유효하지 않음)¶
{
"valid": false,
"errors": [
{
"field": "workers",
"message": "workers must be greater than 0",
"code": "VALIDATION_ERROR"
}
],
"warnings": []
}
예제¶
# 적용 전 검증
curl -X POST http://localhost:8080/admin/config/validate \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"section": "rate_limiting",
"config": {
"enabled": true,
"requests_per_minute": 500
}
}'
설정 적용¶
보류 중인 설정 변경 사항을 즉시 적용합니다 (핫 리로드 트리거).
요청 본문¶
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
sections |
array | 아니오 | 적용할 특정 섹션 (기본값: 모든 보류 중) |
force |
boolean | 아니오 | 경고가 있어도 강제 적용 (기본값: false) |
응답¶
{
"success": true,
"applied_sections": ["logging", "rate_limiting"],
"version": 7,
"results": {
"logging": {
"status": "applied",
"hot_reload_type": "immediate"
},
"rate_limiting": {
"status": "applied",
"hot_reload_type": "immediate"
}
}
}
예제¶
curl -X POST http://localhost:8080/admin/config/apply \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sections": ["logging"]
}'
설정 저장/복원 API¶
설정 내보내기¶
지정된 형식으로 현재 설정을 내보냅니다.
요청 본문¶
{
"format": "yaml",
"sections": ["server", "backends", "logging"],
"include_sensitive": false,
"include_defaults": true
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
format |
string | 예 | 출력 형식: yaml, json, 또는 toml |
sections |
array | 아니오 | 내보낼 섹션 (기본값: 전체) |
include_sensitive |
boolean | 아니오 | 마스킹 안 된 민감 데이터 포함 (기본값: false) |
include_defaults |
boolean | 아니오 | 기본값 포함 (기본값: true) |
응답¶
{
"format": "yaml",
"content": "server:\n bind_address: \"0.0.0.0:8080\"\n workers: 4\n\nbackends:\n - name: openai\n url: https://api.openai.com\n api_key: \"sk-***abcd\"\n",
"exported_at": "2025-12-13T10:30:00Z",
"sections_exported": ["server", "backends", "logging"]
}
예제¶
# YAML로 내보내기
curl -X POST http://localhost:8080/admin/config/export \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"format": "yaml"}' | jq -r '.content' > config-backup.yaml
# JSON으로 내보내기
curl -X POST http://localhost:8080/admin/config/export \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"format": "json"}' | jq -r '.content' > config-backup.json
# 특정 섹션 내보내기
curl -X POST http://localhost:8080/admin/config/export \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"format": "yaml",
"sections": ["backends", "rate_limiting"]
}'
설정 가져오기¶
콘텐츠에서 설정을 가져오고 적용합니다.
요청 본문¶
{
"format": "yaml",
"content": "logging:\n level: info\n format: json\n",
"apply": true,
"dry_run": false,
"merge": true
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
format |
string | 예 | 콘텐츠 형식: yaml, json, 또는 toml |
content |
string | 예 | 설정 콘텐츠 (최대 1MB) |
apply |
boolean | 아니오 | 검증 후 적용 (기본값: true) |
dry_run |
boolean | 아니오 | 적용 없이 검증만 (기본값: false) |
merge |
boolean | 아니오 | 기존 설정과 병합 (기본값: false) |
응답¶
{
"success": true,
"message": "Configuration imported and applied",
"version": 8,
"validation": {
"valid": true,
"errors": [],
"warnings": []
},
"sections_imported": ["logging"],
"applied": true
}
예제¶
# 파일에서 가져오기
curl -X POST http://localhost:8080/admin/config/import \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"format\": \"yaml\",
\"content\": $(cat config-backup.yaml | jq -Rs .),
\"apply\": true
}"
# 가져오기 미리보기 (dry run)
curl -X POST http://localhost:8080/admin/config/import \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"format": "yaml",
"content": "logging:\n level: debug\n",
"dry_run": true
}'
설정 히스토리 가져오기¶
설정 변경 히스토리를 확인합니다.
쿼리 파라미터¶
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
limit |
integer | 아니오 | 반환할 항목 수 (기본값: 20, 최대: 100) |
offset |
integer | 아니오 | 건너뛸 항목 수 (기본값: 0) |
section |
string | 아니오 | 섹션 이름으로 필터링 |
응답¶
{
"history": [
{
"version": 8,
"timestamp": "2025-12-13T10:30:00Z",
"sections_changed": ["logging"],
"source": "api",
"user": "admin",
"description": "Updated logging level to debug",
"rollback_available": true
},
{
"version": 7,
"timestamp": "2025-12-13T10:25:00Z",
"sections_changed": ["rate_limiting"],
"source": "api",
"user": "admin",
"description": "Increased rate limit to 200 rpm",
"rollback_available": true
},
{
"version": 6,
"timestamp": "2025-12-13T09:00:00Z",
"sections_changed": ["backends"],
"source": "file_reload",
"user": "system",
"description": "Configuration file changed",
"rollback_available": true
}
],
"total_entries": 8,
"current_version": 8
}
예제¶
# 최근 히스토리 가져오기
curl -s http://localhost:8080/admin/config/history \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
# 특정 섹션 히스토리 가져오기
curl -s "http://localhost:8080/admin/config/history?section=backends&limit=10" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
설정 롤백¶
이전 설정 버전으로 롤백합니다.
경로 파라미터¶
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
version |
integer | 예 | 롤백할 버전 번호 |
요청 본문¶
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
sections |
array | 아니오 | 롤백할 특정 섹션 (기본값: 변경된 모든 섹션) |
dry_run |
boolean | 아니오 | 적용 없이 미리보기 (기본값: false) |
응답¶
{
"success": true,
"message": "Rolled back to version 5",
"previous_version": 8,
"new_version": 9,
"sections_rolled_back": ["logging", "rate_limiting"],
"changes": {
"logging": {
"level": {
"from": "debug",
"to": "info"
}
}
}
}
예제¶
# 버전 5로 롤백
curl -X POST http://localhost:8080/admin/config/rollback/5 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
# 롤백 미리보기 (dry run)
curl -X POST http://localhost:8080/admin/config/rollback/5 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"dry_run": true}'
백엔드 관리 API¶
백엔드 추가¶
새 백엔드를 동적으로 추가합니다.
요청 본문¶
{
"name": "new-ollama",
"url": "http://192.168.1.100:11434",
"weight": 1,
"models": ["llama3.2", "mistral"],
"api_key": "optional-key",
"enabled": true,
"health_check": {
"enabled": true,
"path": "/v1/models"
}
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | 예 | 고유한 백엔드 이름 (영숫자, -, _) |
type |
string | 아니오 | 백엔드 타입: openai, azure, vllm, ollama, anthropic, gemini, llamacpp, generic. 기본값: generic (자동 감지) |
url |
string | 예 | 백엔드 URL (http:// 또는 https://) |
weight |
integer | 아니오 | 로드 밸런싱 가중치 (기본값: 1) |
models |
array | 아니오 | 이 백엔드가 제공하는 모델 목록 |
api_key |
string | 아니오 | 백엔드 인증용 API 키 |
enabled |
boolean | 아니오 | 백엔드 활성화 여부 (기본값: true) |
백엔드 타입 자동 감지¶
type이 지정되지 않거나 generic으로 설정된 경우, 라우터는 백엔드의 /v1/models 엔드포인트를 자동으로 탐색하여 백엔드 타입을 감지합니다. 현재 자동 감지가 지원되는 백엔드:
- llama.cpp: 응답의
owned_by: "llamacpp"또는 llama.cpp 전용 메타데이터 필드로 식별
덕분에 명시적인 타입 설정 없이도 llama.cpp 백엔드를 통합할 수 있습니다:
# llama.cpp 백엔드 - 타입 자동 감지
curl -X POST http://localhost:8080/admin/backends \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "local-llama",
"url": "http://localhost:8080"
}'
응답¶
{
"success": true,
"message": "Backend 'new-ollama' added successfully",
"backend": {
"name": "new-ollama",
"url": "http://192.168.1.100:11434",
"weight": 1,
"models": ["llama3.2", "mistral"],
"enabled": true,
"health_status": "unknown"
}
}
예제¶
curl -X POST http://localhost:8080/admin/backends \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "new-backend",
"url": "http://192.168.1.100:11434",
"weight": 2,
"models": ["llama3.2"]
}'
백엔드 가져오기¶
특정 백엔드의 설정을 가져옵니다.
응답¶
{
"name": "openai",
"url": "https://api.openai.com",
"api_key": "sk-***abcd",
"weight": 1,
"models": ["gpt-4", "gpt-3.5-turbo"],
"enabled": true,
"health_status": "healthy",
"stats": {
"total_requests": 1250,
"failed_requests": 12,
"average_latency_ms": 150,
"last_used": "2025-12-13T10:29:55Z"
}
}
예제¶
백엔드 업데이트¶
백엔드 설정을 업데이트합니다.
요청 본문¶
{
"url": "https://api.openai.com",
"weight": 2,
"models": ["gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"],
"enabled": true
}
응답¶
{
"success": true,
"message": "Backend 'openai' updated successfully",
"backend": {
"name": "openai",
"url": "https://api.openai.com",
"weight": 2,
"models": ["gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"],
"enabled": true
}
}
예제¶
curl -X PUT http://localhost:8080/admin/backends/openai \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"weight": 3,
"models": ["gpt-4", "gpt-4-turbo"]
}'
백엔드 삭제¶
라우터에서 백엔드를 제거합니다.
쿼리 파라미터¶
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
force |
boolean | 아니오 | 활성 연결이 있어도 강제 삭제 |
응답¶
{
"success": true,
"message": "Backend 'old-backend' removed successfully",
"removed_backend": "old-backend"
}
참고사항¶
- 마지막 백엔드 삭제 허용: 라우터는 백엔드가 없는 상태로도 운영할 수 있습니다. 마지막 백엔드가 삭제되면:
/v1/models는 빈 목록을 반환- 라우팅 요청은 503 "No backends available" 반환
POST /admin/backends를 통해 새 백엔드 추가 가능
예제¶
curl -X DELETE http://localhost:8080/admin/backends/old-backend \
-H "Authorization: Bearer $ADMIN_TOKEN"
# 강제 삭제
curl -X DELETE "http://localhost:8080/admin/backends/old-backend?force=true" \
-H "Authorization: Bearer $ADMIN_TOKEN"
백엔드 가중치 업데이트¶
로드 밸런싱을 위한 백엔드 가중치만 업데이트합니다.
요청 본문¶
응답¶
{
"success": true,
"message": "Backend 'openai' weight updated to 5",
"previous_weight": 2,
"new_weight": 5
}
예제¶
curl -X PUT http://localhost:8080/admin/backends/openai/weight \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"weight": 5}'
백엔드 모델 업데이트¶
백엔드의 모델 목록을 업데이트합니다.
요청 본문¶
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
models |
array | 예 | 모델 이름 목록 |
append |
boolean | 아니오 | 기존 목록에 추가 (기본값: false, 대체) |
응답¶
{
"success": true,
"message": "Backend 'openai' models updated",
"models": ["gpt-4", "gpt-4-turbo", "gpt-4o", "gpt-3.5-turbo"]
}
예제¶
# 모델 대체
curl -X PUT http://localhost:8080/admin/backends/openai/models \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"models": ["gpt-4", "gpt-4o"]}'
# 모델 추가
curl -X PUT http://localhost:8080/admin/backends/openai/models \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"models": ["gpt-4.5-turbo"], "append": true}'
API 키 관리 API¶
API 키 관리 API로 사용자별 API 키를 런타임에 발급하고 조회하며, 수정·교체·활성화·비활성화·폐기할 수 있습니다. 여덟 개 엔드포인트 모두 /admin/api-keys 하위에 마운트되며, 나머지 Admin API와 동일한 관리자 인증을 요구합니다.
이 엔드포인트들은 들어오는 클라이언트 요청을 인증할 때 쓰는 키 저장소를 그대로 다룹니다. 여기서 만든 키는 클라이언트가 Authorization: Bearer <key> 헤더로 곧바로 사용할 수 있으며, 설정된 인증 모드의 영향을 받습니다(아래 인증 모드와 클라이언트 사용법 참조).
API 키 객체¶
각 API 키는 ApiKeyConfig 레코드로 표현됩니다. 아래 필드는 config.yaml 인라인, 외부 키 파일, 또는 생성/수정 엔드포인트로 설정합니다.
| 필드 | 타입 | 설명 |
|---|---|---|
key |
string | 비밀 키 값. 지정하지 않으면 암호학적으로 생성됩니다(형식 sk-<base64url>). 생성·교체 시점에 단 한 번만 전체 값이 반환되고, 그 외에는 모두 마스킹됩니다. |
id |
string | 키 고유 식별자(1~128자). 모든 /admin/api-keys/{id} 경로에 쓰입니다. |
user_id |
string | 연결된 사용자 식별자(1~128자). 사용자별 사용량 통계에 나타납니다. |
organization_id |
string | 연결된 조직 식별자(1~128자). |
name |
string 또는 없음 | 사람이 읽는 라벨(최대 256자). |
description |
string 또는 없음 | 키에 대한 메모(최대 1024자). |
scopes |
문자열 배열 | 키에 부여된 권한. 흔히 read, write, files, admin을 씁니다. 키를 생성할 때 최소 한 개가 필요합니다. |
rate_limit |
정수 또는 없음 | 분당 요청 수로 지정하는 키별 속도 제한. 이 키에 한해 전역 제한을 덮어씁니다. |
enabled |
boolean | 키 활성 여부. 비활성 키는 만료 검사 이전에 인증이 실패합니다. |
created_at |
string (ISO 8601) | 생성 시각. |
expires_at |
string (ISO 8601) 또는 없음 | 만료 시각. 이 시각을 지난 키는 enabled 값과 무관하게 자동으로 무효가 됩니다. |
annotations |
객체(문자열→문자열) 또는 없음 | 자유 형식 메타데이터 맵. 권장 표준 키는 email, uuid, owner, team, environment입니다. 운영자가 지정한 어노테이션 키 허용 목록이 api_key_info Prometheus 메트릭의 라벨로 노출됩니다(값은 정제 후 방출). |
allowed_backends |
문자열 배열 또는 없음 | 키별 백엔드 허용 목록. 비어 있지 않으면 이 키로 인증한 요청은 목록에 이름이 있는 백엔드로만 라우팅됩니다. 비어 있거나 없으면 제한이 없습니다. 정확히 대소문자까지 일치해야 하며, 서비스할 수 없는 요청은 403 Forbidden으로 거부됩니다. |
키는 enabled이면서 expires_at을 지나지 않았을 때 유효합니다. 목록 조회 엔드포인트는 이 규칙으로 계산한 active, expired, disabled 개수를 함께 보고합니다.
키 마스킹¶
전체 key 값은 딱 두 곳에서만 반환됩니다. 생성 응답(POST /admin/api-keys)과 교체 응답(POST /admin/api-keys/{id}/rotate)입니다. 나머지 응답은 sk-***abcd 형태의 masked_key를 돌려주는데, sk- 접두사와 마지막 네 글자를 유지합니다. 로그도 항상 마스킹된 형태를 씁니다.
인증 모드와 클라이언트 사용법¶
api_keys.mode 설정은 유효한 키가 없는 클라이언트 요청을 라우터가 어떻게 다룰지 결정합니다.
| 모드 | 동작 |
|---|---|
permissive (기본값) |
유효한 키가 있는 요청은 인증·귀속되고, 키가 없는 요청도 통과합니다. 점진적 도입에 적합합니다. |
blocking |
모든 API 요청에 유효한 키가 있어야 합니다. 키가 없으면 401 Unauthorized를 받습니다. |
config.yaml에서 모드를 설정합니다.
api_keys:
mode: blocking # "permissive" (기본값) | "blocking"
persistence_file: ~/.config/continuum-router/runtime-keys.yaml
api_keys:
- key: "sk-prod-..."
id: "key-1"
user_id: "user-1"
organization_id: "org-1"
scopes: ["read", "write"]
클라이언트는 발급받은 키를 베어러 토큰으로 보내 인증합니다.
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer sk-the-issued-key-value" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}'
mode 설정은 핫 리로드됩니다. permissive와 blocking 사이를 바꿔도 재시작 없이 적용됩니다.
영속화와 핫 리로드¶
이 엔드포인트로 만들거나 고친 키는 인메모리 키 저장소에 있습니다. api_keys.persistence_file을 지정하면 런타임 변경이 그 파일에 기록되고(틸드 확장 지원) 다음 시작 때 복원되므로, 관리자가 만든 키가 재시작 후에도 남습니다. persistence_file이 없으면 런타임 키는 인메모리 전용이라 재시작 시 사라집니다. 인라인 설정이나 api_keys_file에서 읽은 키는 읽기 전용 소스이며 설정 핫 리로드 때 다시 적재됩니다.
API 키 목록 조회¶
모든 API 키를 값이 마스킹된 상태로 반환하고, 활성·만료·비활성 개수 요약을 함께 제공합니다.
응답¶
{
"keys": [
{
"id": "key-1",
"masked_key": "sk-***A1aB",
"user_id": "user-1",
"organization_id": "org-1",
"name": "Production key",
"scopes": ["read", "write"],
"rate_limit": 600,
"is_active": true,
"expires_at": null,
"created_at": "2026-03-05T10:30:00Z",
"is_expired": false,
"allowed_backends": ["openai", "anthropic"]
}
],
"summary": {
"total": 1,
"active": 1,
"expired": 0,
"disabled": 0
}
}
예제¶
API 키 생성¶
새 API 키를 만듭니다. key를 생략하면 라우터가 암호학적으로 무작위 값을 생성합니다. 전체 키 값은 이 응답에서만 반환됩니다.
요청 본문¶
{
"id": "key-acme-1",
"user_id": "user-acme",
"organization_id": "org-acme",
"name": "Acme integration",
"description": "Server-to-server key for the Acme integration",
"scopes": ["read", "write"],
"rate_limit": 600,
"enabled": true,
"expires_at": "2027-01-01T00:00:00Z",
"allowed_backends": ["openai"]
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
id |
string | 예 | 키 고유 식별자(1~128자). |
user_id |
string | 예 | 연결된 사용자 식별자(비어 있으면 안 됨). |
organization_id |
string | 예 | 연결된 조직 식별자(비어 있으면 안 됨). |
key |
string | 아니오 | 사용자 지정 키 값. 생략하면 새 값을 생성합니다. |
name |
string | 아니오 | 사람이 읽는 라벨(최대 256자). |
description |
string | 아니오 | 키에 대한 메모(최대 1024자). |
scopes |
배열 | 아니오 | 권한. 기본값은 ["read", "write"]이며 최소 한 개가 있어야 합니다. |
rate_limit |
정수 | 아니오 | 분당 요청 수로 지정하는 키별 속도 제한. |
enabled |
boolean | 아니오 | 키 활성 여부. 기본값은 true. |
expires_at |
string (ISO 8601) | 아니오 | 만료 시각. |
allowed_backends |
배열 | 아니오 | 키별 백엔드 허용 목록. 비어 있거나 생략하면 제한이 없습니다. |
응답¶
201 Created를 반환합니다. key 필드가 전체 값이며 여기서만 노출됩니다.
{
"key": "sk-G7q2...full-value...A1",
"masked_key": "sk-***A1aB",
"id": "key-acme-1",
"user_id": "user-acme",
"organization_id": "org-acme",
"name": "Acme integration",
"scopes": ["read", "write"],
"rate_limit": 600,
"enabled": true,
"created_at": "2026-03-05T10:30:00Z",
"expires_at": "2027-01-01T00:00:00Z",
"allowed_backends": ["openai"]
}
오류 응답¶
400 Bad Request:user_id/organization_id가 비었거나, 스코프가 없거나,name/description이 길이 제한을 넘긴 경우.409 Conflict: 같은id의 키가 이미 있는 경우.507 Insufficient Storage: 최대 키 개수(10,000)에 도달한 경우.
예제¶
curl -X POST http://localhost:8080/admin/api-keys \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "key-acme-1",
"user_id": "user-acme",
"organization_id": "org-acme",
"scopes": ["read", "write"],
"rate_limit": 600
}'
API 키 조회¶
id로 키 하나를 값이 마스킹된 상태로 반환합니다.
응답¶
{
"id": "key-acme-1",
"masked_key": "sk-***A1aB",
"user_id": "user-acme",
"organization_id": "org-acme",
"name": "Acme integration",
"scopes": ["read", "write"],
"rate_limit": 600,
"is_active": true,
"created_at": "2026-03-05T10:30:00Z",
"expires_at": "2027-01-01T00:00:00Z",
"is_expired": false,
"is_valid": true,
"allowed_backends": ["openai"]
}
is_active, is_expired, is_valid는 계산된 값으로, is_valid는 키가 활성이면서 만료되지 않았을 때만 true입니다.
오류 응답¶
404 Not Found: 해당id의 키가 없는 경우.
예제¶
curl -s http://localhost:8080/admin/api-keys/key-acme-1 \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
API 키 수정¶
기존 키의 속성을 하나 이상 수정합니다. 본문에 있는 필드만 바뀌고 생략한 필드는 그대로 둡니다. 키 값 자체는 이 엔드포인트로 바뀌지 않습니다(그 용도는 교체).
요청 본문¶
{
"name": "Acme integration (renamed)",
"scopes": ["read"],
"rate_limit": 300,
"enabled": true,
"expires_at": "2027-06-01T00:00:00Z",
"allowed_backends": ["openai", "anthropic"]
}
| 필드 | 타입 | 설명 |
|---|---|---|
name |
string | 새 라벨. |
scopes |
배열 | 교체할 스코프 목록. |
rate_limit |
정수 | 새 키별 속도 제한. |
enabled |
boolean | 키 활성화/비활성화. |
expires_at |
string (ISO 8601) | 새 만료 시각. |
allowed_backends |
배열 | 백엔드 허용 목록. 생략(null)하면 그대로 두고, 빈 배열이면 모든 제한을 해제하며, 비어 있지 않으면 목록을 교체합니다. |
응답¶
{
"success": true,
"action": "update",
"key": {
"id": "key-acme-1",
"masked_key": "sk-***A1aB",
"user_id": "user-acme",
"organization_id": "org-acme",
"name": "Acme integration (renamed)",
"scopes": ["read"],
"rate_limit": 300,
"is_active": true,
"created_at": "2026-03-05T10:30:00Z",
"expires_at": "2027-06-01T00:00:00Z",
"is_valid": true,
"allowed_backends": ["openai", "anthropic"]
}
}
오류 응답¶
404 Not Found: 해당id의 키가 없는 경우.
예제¶
curl -X PUT http://localhost:8080/admin/api-keys/key-acme-1 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"rate_limit": 300, "scopes": ["read"]}'
API 키 삭제¶
키를 영구히 폐기하고 제거합니다. 삭제 후에는 옛 값을 제시하는 클라이언트의 인증이 실패합니다. 이 작업은 되돌릴 수 없습니다.
응답¶
오류 응답¶
404 Not Found: 해당id의 키가 없는 경우.
예제¶
curl -X DELETE http://localhost:8080/admin/api-keys/key-acme-1 \
-H "Authorization: Bearer $ADMIN_TOKEN"
API 키 교체¶
id와 다른 속성은 그대로 두고 기존 키의 비밀 값만 새로 생성합니다. 이전 값은 즉시 동작을 멈춥니다. 새 값은 이 응답에서만 반환됩니다.
응답¶
{
"success": true,
"action": "rotate",
"id": "key-acme-1",
"new_key": "sk-Hq9z...new-full-value...B2",
"masked_key": "sk-***B2cD",
"warning": "Store this key securely. It will not be shown again."
}
오류 응답¶
404 Not Found: 해당id의 키가 없는 경우.
예제¶
curl -X POST http://localhost:8080/admin/api-keys/key-acme-1/rotate \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
API 키 활성화¶
키를 활성 상태로 표시합니다. 다시 활성화한 키는 만료되지 않았다면 또 인증됩니다.
응답¶
오류 응답¶
404 Not Found: 해당id의 키가 없는 경우.
예제¶
curl -X POST http://localhost:8080/admin/api-keys/key-acme-1/enable \
-H "Authorization: Bearer $ADMIN_TOKEN"
API 키 비활성화¶
키를 삭제하지 않고 비활성 상태로 표시합니다. 비활성 키는 인증에 실패하지만 설정은 그대로 유지되므로 나중에 다시 활성화할 수 있습니다. 삭제 대신 되돌릴 수 있는 일시 정지가 필요할 때 씁니다.
응답¶
오류 응답¶
404 Not Found: 해당id의 키가 없는 경우.
예제¶
curl -X POST http://localhost:8080/admin/api-keys/key-acme-1/disable \
-H "Authorization: Bearer $ADMIN_TOKEN"
통계 API¶
통계 API는 StatsCollector가 수집한 집계 요청 메트릭을 노출합니다. 모든 엔드포인트가 /admin/stats 하위에 마운트되며 나머지 Admin API와 동일한 인증을 공유합니다. 전체·모델별·백엔드별 분류와 함께 API 키별·사용자별 사용량도 추적합니다(API 키별·사용자별 통계 참조).
통계 수집은 기본적으로 활성화되어 있습니다. config.yaml의 admin.stats 섹션을 통해 설정하거나 비활성화할 수 있습니다:
admin:
stats:
enabled: true # 수집 활성화/비활성화 (기본값: true)
retention_window: 24h # 윈도우 쿼리용 링 버퍼 보존 기간 (기본값: 24h)
token_tracking: true # 토큰 사용량 파싱 (기본값: true)
series_retention_days: 30 # /series 엔드포인트용 일별 사용량 버킷 보존 일수 (기본값: 30)
persistence:
enabled: true # 재시작 시 통계 영속 (기본값: true)
path: ./data/stats.json # 스냅샷 파일 경로 (기본값: ./data/stats.json)
snapshot_interval: 5m # 주기적 스냅샷 간격 (기본값: 5m)
max_age: 7d # 시작 시 이보다 오래된 스냅샷은 폐기 (기본값: 7d)
retention_window와 token_tracking 설정은 핫 리로드를 지원합니다: 재시작 없이 즉시 변경 사항이 적용됩니다.
통계 영속화¶
persistence 하위 섹션이 존재하고 enabled가 true이면, 라우터는 주기적으로 통계 스냅샷을 디스크에 저장하고 시작 시 복원합니다. 따라서 요청 카운터, 모델별 분류, 레이턴시 링 버퍼가 재시작 후에도 유지됩니다.
동작 방식:
- 시작 시 라우터가 스냅샷 파일을 읽고 모든 카운터와 링 버퍼 레코드를 복원합니다. 업타임은 각 재시작마다 초기화됩니다.
- 백그라운드 태스크가
snapshot_interval마다 새 스냅샷을 저장합니다. 손상 방지를 위해 원자적 쓰기(임시 파일 + 이름 변경)를 사용합니다. - 정상 종료(SIGTERM/SIGINT) 시, 프로세스 종료 전에 최종 스냅샷이 저장됩니다.
- 스냅샷 파일이 없거나, 손상되었거나,
max_age보다 오래된 경우 라우터는 새 카운터로 시작하고 경고 또는 정보 메시지를 로깅합니다.
snapshot_interval과 max_age에 지원되는 기간 형식:
| 형식 | 예제 | 의미 |
|---|---|---|
Xs |
30s |
30초 |
Xm |
5m |
5분 |
Xh |
1h |
1시간 |
Xd |
7d |
7일 |
max_age를 "0" 또는 ""으로 설정하면 유효 기간 검사를 비활성화합니다 (기간에 관계없이 항상 복원).
전체 통계 조회¶
전체, 모델별, 백엔드별 통계를 반환합니다.
쿼리 파라미터¶
| 파라미터 | 타입 | 설명 |
|---|---|---|
window |
string | 선택적 시간 윈도우 필터. 허용 형식: 30m, 1h, 24h, 7d. 전체 기간 합계는 생략. |
응답¶
{
"uptime_seconds": 3600,
"window": "all",
"overall": {
"total_requests": 1500,
"successful_requests": 1480,
"failed_requests": 20,
"avg_latency_ms": 145.3,
"p50_latency_ms": 120.0,
"p95_latency_ms": 380.0,
"p99_latency_ms": 750.0,
"total_prompt_tokens": 450000,
"total_completion_tokens": 180000,
"total_tokens": 630000,
"tokens_per_sec_avg": 87.4
},
"models": [
{
"model_id": "gpt-4",
"total_requests": 900,
"successful_requests": 895,
"failed_requests": 5,
"total_prompt_tokens": 270000,
"total_completion_tokens": 108000,
"total_tokens": 378000,
"avg_latency_ms": 160.2,
"avg_tokens_per_sec": 92.1,
"last_used": "2026-03-05T10:30:00Z"
}
],
"backends": [
{
"backend_name": "openai",
"total_requests": 900,
"successful_requests": 895,
"failed_requests": 5,
"avg_latency_ms": 160.2,
"health_status": "healthy"
}
]
}
예제¶
# 전체 기간 통계
curl -s http://localhost:8080/admin/stats \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
# 지난 1시간만
curl -s "http://localhost:8080/admin/stats?window=1h" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
모델별 통계 조회¶
모델별 분류만 반환합니다 (전체 통계 응답의 부분집합).
응답¶
{
"models": [
{
"model_id": "gpt-4",
"total_requests": 900,
"successful_requests": 895,
"failed_requests": 5,
"total_prompt_tokens": 270000,
"total_completion_tokens": 108000,
"total_tokens": 378000,
"avg_latency_ms": 160.2,
"avg_tokens_per_sec": 92.1,
"last_used": "2026-03-05T10:30:00Z"
}
]
}
모델은 total_requests 기준 내림차순으로 정렬됩니다.
예제¶
curl -s http://localhost:8080/admin/stats/models \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.models[].model_id'
백엔드별 통계 조회¶
백엔드별 분류만 반환합니다. health_status 필드는 헬스 체커에서 채워집니다 ("healthy", "unhealthy", 또는 헬스 체크가 비활성화된 경우 "unknown").
응답¶
{
"backends": [
{
"backend_name": "openai",
"total_requests": 900,
"successful_requests": 895,
"failed_requests": 5,
"avg_latency_ms": 160.2,
"health_status": "healthy"
}
]
}
백엔드는 total_requests 기준 내림차순으로 정렬됩니다.
예제¶
API 키별·사용자별 통계¶
이 엔드포인트들은 각 요청을 인증한 API 키와 그 키에 연결된 사용자를 기준으로 사용량을 나눕니다. 모델별 통계 조회, 백엔드별 통계 조회와 나란히 놓이며, 같은 컬렉터를 쓰되 묶는 기준만 다릅니다.
식별자와 버킷 규칙¶
- 집계 범위: 모든 추론 표면이 이 통계에 기여합니다 —
/v1/chat/completions,/anthropic/v1/messages, 그리고 OpenAI Responses API(/v1/responses, 패스스루·Chat Completions 변환·Anthropic 변환 전략 포함). 성공한 비스트리밍 요청은 전체 토큰 사용량을 담고, 스트리밍 요청은 연결 시점에 기록됩니다(요청 수와 API 키별·사용자별 귀속은 기록하되, 토큰 합계는 스트림 종료 시에만 알 수 있어 생략). api_key_id는 원본 키가 아니라 역산 불가능한 파생 식별자입니다.api_key_idPrometheus 라벨과 같은 값이며, 발급된 키의id에 대응합니다. 사용자별 엔드포인트는 일치한 키에 연결된user_id를 기준으로 묶습니다. 파생api_key_id는metrics피처가 컴파일되어 있어야 하며, 없으면 API 키별 귀속이"anonymous"버킷으로 합쳐집니다(사용자별 귀속은 키의user_id를 직접 읽으므로 영향받지 않습니다).- 키가 없는(또는 연결된 사용자가 없는) 요청은
"anonymous"버킷으로 들어갑니다. - 각 차원은 서로 다른 식별자 1000개(예약 버킷 제외)의 카디널리티 상한을 둡니다. 상한에 도달하면 그 뒤의 새 식별자는
"unknown"오버플로 버킷으로 접혀 들어가, 사용량이 합계로는 계속 집계됩니다. window쿼리 파라미터는GET /admin/stats와의 일관성을 위해 받아들여 응답에 그대로 돌려주지만, API 키별·사용자별 집계는GET /admin/stats/models와 똑같이 전체 기간 합계입니다. 식별자는 요청 핫 패스 밖에서 결정되므로, 시간 필터 레이턴시 백분위에 쓰는 윈도우 링 버퍼 레코드에는 들어 있지 않습니다.
ApiKeyStats와 UserStats 객체는 같은 모양입니다.
| 필드 | 타입 | 설명 |
|---|---|---|
api_key_id / user_id |
string | 파생 키 식별자 또는 사용자 식별자. |
total_requests |
정수 | 이 식별자에 귀속된 총 요청 수. |
successful_requests |
정수 | 성공한 요청 수. |
failed_requests |
정수 | 실패한 요청 수. |
total_prompt_tokens |
정수 | 소비한 프롬프트 토큰 수. |
total_completion_tokens |
정수 | 생성한 컴플리션 토큰 수. |
total_tokens |
정수 | 프롬프트와 컴플리션 토큰의 합. |
avg_latency_ms |
숫자 | 평균 레이턴시(밀리초). |
avg_tokens_per_sec |
숫자 | 평균 생성 처리량(초당 토큰). |
last_used |
string (ISO 8601) 또는 null | 가장 최근 요청 시각. 사용 이력이 없으면 null. |
API 키별 통계 조회¶
추적 중인 API 키마다 항목 하나씩을 total_requests 기준 내림차순으로 반환합니다.
쿼리 파라미터¶
| 파라미터 | 타입 | 설명 |
|---|---|---|
window |
string | 받아들여 window 필드에 그대로 돌려주지만, 전체 기간 집계를 필터링하지는 않습니다. |
응답¶
{
"window": "all",
"api_keys": [
{
"api_key_id": "k_3f9a1c",
"total_requests": 1200,
"successful_requests": 1185,
"failed_requests": 15,
"total_prompt_tokens": 360000,
"total_completion_tokens": 144000,
"total_tokens": 504000,
"avg_latency_ms": 152.7,
"avg_tokens_per_sec": 88.3,
"last_used": "2026-03-05T10:30:00Z"
},
{
"api_key_id": "anonymous",
"total_requests": 80,
"successful_requests": 80,
"failed_requests": 0,
"total_prompt_tokens": 12000,
"total_completion_tokens": 4800,
"total_tokens": 16800,
"avg_latency_ms": 131.0,
"avg_tokens_per_sec": 90.1,
"last_used": "2026-03-05T10:28:00Z"
}
]
}
예제¶
curl -s http://localhost:8080/admin/stats/api-keys \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
# window 파라미터는 받아들여 그대로 돌려주지만 집계를 바꾸지는 않습니다
curl -s "http://localhost:8080/admin/stats/api-keys?window=24h" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.window'
API 키별 통계 조회 (ID)¶
단일 api_key_id(목록 엔드포인트가 돌려주는 파생 식별자이며 원본 키가 아님)의 통계를 반환합니다. 해당 식별자에 기록된 사용량이 없으면 404 Not Found를 반환합니다.
응답¶
{
"window": "all",
"api_key": {
"api_key_id": "k_3f9a1c",
"total_requests": 1200,
"successful_requests": 1185,
"failed_requests": 15,
"total_prompt_tokens": 360000,
"total_completion_tokens": 144000,
"total_tokens": 504000,
"avg_latency_ms": 152.7,
"avg_tokens_per_sec": 88.3,
"last_used": "2026-03-05T10:30:00Z"
}
}
예제¶
curl -s http://localhost:8080/admin/stats/api-keys/k_3f9a1c \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
사용자별 통계 조회¶
추적 중인 사용자 식별자(일치한 키에 연결된 user_id)마다 항목 하나씩을 total_requests 기준 내림차순으로 반환합니다. 필드와 버킷 규칙은 API 키별 엔드포인트와 같습니다.
응답¶
{
"window": "all",
"users": [
{
"user_id": "user-acme",
"total_requests": 1200,
"successful_requests": 1185,
"failed_requests": 15,
"total_prompt_tokens": 360000,
"total_completion_tokens": 144000,
"total_tokens": 504000,
"avg_latency_ms": 152.7,
"avg_tokens_per_sec": 88.3,
"last_used": "2026-03-05T10:30:00Z"
}
]
}
예제¶
사용자별 통계 조회 (ID)¶
단일 user_id의 통계를 반환합니다. 해당 식별자에 기록된 사용량이 없으면 404 Not Found를 반환합니다.
응답¶
{
"window": "all",
"user": {
"user_id": "user-acme",
"total_requests": 1200,
"successful_requests": 1185,
"failed_requests": 15,
"total_prompt_tokens": 360000,
"total_completion_tokens": 144000,
"total_tokens": 504000,
"avg_latency_ms": 152.7,
"avg_tokens_per_sec": 88.3,
"last_used": "2026-03-05T10:30:00Z"
}
}
예제¶
curl -s http://localhost:8080/admin/stats/users/user-acme \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
모델별 분류 및 일별 사용량 시계열¶
이 식별자별 세부 조회 기능은 대시보드 위젯 두 가지를 구현합니다. 모델별 분류("모델별 토큰" 도넛 차트)와 일별 사용량 추이("시간에 따른 사용량" 차트)입니다. 두 차원은 (식별자, 모델, 날짜) 큐브가 아니라 독립적으로 추적하므로 카디널리티가 제한된 채로 유지됩니다.
적용 범위와 규칙은 API 키별·사용자별 통계를 그대로 따릅니다.
- 토큰 수와 요청 수만 추적합니다. 비용 필드는 없으며, 대시보드가 자체 단가표와 토큰 수를 결합해 비용을 계산합니다.
api_key_id는 역산 불가능한 파생 식별자(원본 키가 아님)이고,user_id는 일치한 키에 연결된 사용자입니다. 알 수 없는 식별자를 조회하면 404가 아니라 빈 배열과 함께200 OK를 반환하며, 이는 목록 엔드포인트의 동작과 같습니다.- 새 차원은 각각 카디널리티 상한을 가지며(오버플로는 집계
"unknown"버킷으로 접히고, 식별자별 조회에서는 제외됨), 알 수 없는 모델 라벨은"unknown"입니다.
API 키별 모델 분류 조회¶
단일 api_key_id의 모델별 분류를 models 배열로 반환합니다. 배열 내 각 항목은 GET /admin/stats/models가 반환하는 것과 같은 ModelStats 객체(모델 ID, 요청 수, 프롬프트/컴플리션/전체 토큰, 평균 레이턴시, 평균 초당 토큰, 마지막 사용 시각)이며, total_requests 기준 내림차순으로 정렬됩니다. window 쿼리 파라미터는 받아들여 그대로 돌려주지만, 전체 기간 집계를 필터링하지는 않습니다.
응답¶
{
"api_key_id": "k_3f9a1c",
"window": "all",
"models": [
{
"model_id": "claude-haiku-4-5",
"total_requests": 2,
"successful_requests": 2,
"failed_requests": 0,
"total_prompt_tokens": 374,
"total_completion_tokens": 8,
"total_tokens": 382,
"avg_latency_ms": 975.0,
"avg_tokens_per_sec": 195.9,
"last_used": "2026-06-18T22:11:54Z"
}
]
}
예제¶
curl -s http://localhost:8080/admin/stats/api-keys/k_3f9a1c/models \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
사용자별 모델 분류 조회¶
user_id로 묶는다는 점만 다르고 응답 구조는 API 키별 모델 분류 조회와 같습니다.
응답¶
{
"user_id": "user-acme",
"window": "all",
"models": [
{
"model_id": "claude-haiku-4-5",
"total_requests": 2,
"successful_requests": 2,
"failed_requests": 0,
"total_prompt_tokens": 374,
"total_completion_tokens": 8,
"total_tokens": 382,
"avg_latency_ms": 975.0,
"avg_tokens_per_sec": 195.9,
"last_used": "2026-06-18T22:11:54Z"
}
]
}
예제¶
curl -s http://localhost:8080/admin/stats/users/user-acme/models \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
API 키별 일별 사용량 시계열 조회¶
단일 api_key_id의 일별 사용량 시계열을 반환합니다. UTC 기준 하루 단위 버킷 하나씩으로, date 기준 오름차순으로 정렬됩니다. 버킷은 series_retention_days(기본값 30)일 동안 유지되며, 주기적 스냅샷 태스크가 오래된 날을 정리하고 조회 시에도 필터링하므로 보존 기간을 넘은 날은 반환되지 않습니다.
쿼리 파라미터¶
| 파라미터 | 타입 | 설명 |
|---|---|---|
from |
string | 포함하는 하한. Unix 밀리초(정수) 또는 RFC 3339 타임스탬프. 기본값은 30일 전. |
to |
string | 제외하는 상한. from과 같은 형식. 기본값은 현재 시각. |
interval |
string | 버킷 단위. day만 지원하며, 다른 값은 400 Bad Request를 반환합니다. 기본값 day. |
범위가 역전된 경우(from >= to)에도 400 Bad Request를 반환합니다.
응답¶
{
"api_key_id": "k_3f9a1c",
"interval": "day",
"series": [
{ "date": "2026-06-17", "total_requests": 12, "prompt_tokens": 3600, "completion_tokens": 1440, "total_tokens": 5040 },
{ "date": "2026-06-18", "total_requests": 8, "prompt_tokens": 2400, "completion_tokens": 960, "total_tokens": 3360 }
]
}
예제¶
curl -s "http://localhost:8080/admin/stats/api-keys/k_3f9a1c/series?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z&interval=day" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
사용자별 일별 사용량 시계열 조회¶
user_id로 묶는다는 점만 다르고 응답 구조와 파라미터는 API 키별 시계열 조회와 같습니다.
응답¶
{
"user_id": "user-acme",
"interval": "day",
"series": [
{ "date": "2026-06-17", "total_requests": 12, "prompt_tokens": 3600, "completion_tokens": 1440, "total_tokens": 5040 },
{ "date": "2026-06-18", "total_requests": 8, "prompt_tokens": 2400, "completion_tokens": 960, "total_tokens": 3360 }
]
}
예제¶
curl -s "http://localhost:8080/admin/stats/users/user-acme/series?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
통계 초기화¶
모든 카운터, 모델별 레코드, 백엔드별 레코드, API 키별·사용자별 레코드(모델별 분류 및 일별 시계열 버킷 포함), 레이턴시 링 버퍼를 초기화합니다. 이 작업은 되돌릴 수 없습니다.
응답¶
예제¶
지속 메트릭 로그 API¶
지속 메트릭 로그 API는 로컬 저장소(기본값 SQLite)에 보존된 최근 Prometheus 레지스트리 이력을 노출합니다. 저장소 레이아웃과 보존 기간 산정, 설정 옵션은 지속 메트릭 로그 가이드를 참고하세요.
메트릭 이력 조회¶
[from, to) 반열린 시간 구간 안에서 metric의 과거 샘플을 반환합니다.
쿼리 파라미터¶
| 파라미터 | 필수 | 기본값 | 설명 |
|---|---|---|---|
metric |
yes | — | 메트릭 패밀리 이름(예: http_requests_total). |
from |
no | 현재 시각 − 24시간 | Unix 밀리초(정수) 또는 RFC 3339 타임스탬프. |
to |
no | 현재 시각 | Unix 밀리초(정수) 또는 RFC 3339 타임스탬프. |
limit |
no | 10,000 | 반환 행 수 상한이고 하드 한도는 100,000입니다. |
응답¶
{
"metric": "http_requests_total",
"from_ms": 1715385600000,
"to_ms": 1715472000000,
"row_count": 2,
"limit": 10000,
"samples": [
{
"ts_ms": 1715385600000,
"labels": {"backend": "openai", "endpoint": "/v1/chat/completions"},
"value": 42.0,
"kind": "counter"
}
]
}
히스토그램과 서머리는 패밀리마다 여러 kind 행을 돌려주는데, 자세한 내용은 지속 메트릭 로그 가이드의 샘플 종류 표를 참고하세요.
오류 응답¶
400 Bad Request—metric이 누락·과대 크기이거나 시간 범위가 0 이하인 경우입니다.404 Not Found— 지속 기능이 비활성화된 경우입니다(metrics.persistence.enabled: false).500 Internal Server Error— 저장소 오류입니다.503 Service Unavailable—metrics-persistence피처가 빌드에 포함되지 않은 경우입니다.
예제¶
curl -s 'http://localhost:8080/admin/metrics/history?metric=http_requests_total&limit=100' \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq .
응답 캐시 Admin API¶
응답 캐시 Admin API는 응답 캐시에 대한 통계 및 무효화 작업을 제공합니다. 모든 엔드포인트는 /admin/response-cache 하위에 마운트되며 나머지 Admin API와 동일한 인증이 필요합니다.
응답 캐싱은 YAML 설정의 response_cache 섹션에서 구성합니다. 전체 설정 세부 사항은 응답 캐시 설정 가이드를 참조하세요.
응답 캐시 통계 조회¶
히트/미스 카운트, 메모리 사용량, 설정 요약을 포함한 현재 응답 캐시 통계를 반환합니다.
응답¶
{
"enabled": true,
"backend_type": "memory",
"entries": 42,
"capacity": 1000,
"requests": {
"hit": 120,
"miss": 80,
"skip": 15,
"total": 215
},
"hit_rate": "0.6000",
"evictions": 3,
"size_bytes": 1048576,
"config": {
"backend": "memory",
"ttl": "5m",
"capacity": 1000,
"max_response_size": 1048576,
"max_stream_buffer_size": 10485760
}
}
Redis 백엔드(backend: redis)를 사용하는 경우, 응답에 추가 redis 객체가 포함됩니다:
{
"enabled": true,
"backend_type": "redis",
"entries": 42,
"capacity": 1000,
"requests": { "hit": 120, "miss": 80, "skip": 15, "total": 215 },
"hit_rate": "0.6000",
"evictions": 3,
"size_bytes": 1048576,
"config": { "backend": "redis", "ttl": "5m", "capacity": 1000, "max_response_size": 1048576, "max_stream_buffer_size": 10485760 },
"redis": {
"connections": { "active": 3, "idle": 5 },
"errors": { "connection": 0, "timeout": 0, "other": 0, "total": 0 },
"fallback_active": false
}
}
응답 캐싱이 비활성화된 경우(response_cache.enabled: false 또는 섹션이 없는 경우), enabled는 false, entries와 capacity는 0, config는 null입니다.
응답 필드¶
| 필드 | 타입 | 설명 |
|---|---|---|
enabled |
boolean | 응답 캐싱 활성화 여부 |
backend_type |
string | 활성 캐시 백엔드: "memory" 또는 "redis" |
entries |
integer | 현재 캐시된 항목 수 |
capacity |
integer | 최대 캐시 용량 (LRU 제한) |
requests.hit |
integer | 캐시에서 제공된 요청 수 |
requests.miss |
integer | 캐시 미스 (백엔드 호출, 항목 저장) |
requests.skip |
integer | 캐시 불가 요청 (예: temperature > 0) |
requests.total |
integer | 총 캐시 가능 조회 (hit + miss + skip) |
hit_rate |
string | 롤링 캐시 히트율 (소수 문자열, 예: "0.6000") |
evictions |
integer | 시작 이후 총 LRU 제거 횟수 |
size_bytes |
integer | 캐시된 항목의 대략적인 메모리 사용량 (바이트) |
config |
object 또는 null | 활성 설정 요약; 비활성화 시 null |
redis |
object 또는 없음 | Redis 전용 통계 (backend_type이 "redis"인 경우에만 존재) |
redis.connections.active |
integer | Redis 풀의 활성 연결 수 |
redis.connections.idle |
integer | Redis 풀의 유휴 연결 수 |
redis.errors.connection |
integer | 시작 이후 Redis 연결 오류 수 |
redis.errors.timeout |
integer | 시작 이후 Redis 명령 타임아웃 오류 수 |
redis.errors.other |
integer | 시작 이후 기타 Redis 오류 수 |
redis.errors.total |
integer | 시작 이후 총 Redis 오류 수 |
redis.fallback_active |
boolean | 인메모리 폴백 활성화 여부 |
예제¶
curl -s http://localhost:8080/admin/response-cache/stats \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
응답 캐시 무효화¶
캐시 항목을 삭제합니다. clear_all: true를 통한 전체 캐시 무효화만 지원하며, 모델 또는 테넌트별 대상 무효화는 제공되지 않습니다.
요청 본문¶
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
clear_all |
boolean | 아니오 | true이면 전체 캐시를 삭제합니다. 기본값: false. |
model |
string | 아니오 | 받기는 하지만 현재 무시됩니다. clear_all만 처리됩니다. 256자를 초과할 수 없습니다. |
tenant_id |
string | 아니오 | 받기는 하지만 현재 무시됩니다. clear_all만 처리됩니다. 256자를 초과할 수 없습니다. |
응답 (clear_all: true)¶
응답 (clear_all: false 또는 생략)¶
{
"success": true,
"action": "noop",
"message": "Targeted invalidation by model/tenant_id is not yet supported. Use clear_all: true to clear the entire cache."
}
응답 (캐시 비활성화)¶
예제¶
# 전체 캐시 삭제
curl -X POST http://localhost:8080/admin/response-cache/invalidate \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"clear_all": true}'
KV 캐시 인덱스 Admin API¶
KV 캐시 인덱스 Admin API는 KV 캐시 인덱스 하위 시스템에 대한 통계, 백엔드별 상태, 삭제 작업을 제공합니다. 모든 엔드포인트는 /admin/kv-index 하위에 마운트되며 나머지 Admin API와 동일한 인증이 필요합니다.
KV 캐시 인덱스는 특정 토큰 접두사에 대해 어떤 백엔드가 캐시된 KV 데이터를 보유하고 있는지 추적하여 KV 인식 라우팅을 가능하게 합니다. YAML 설정의 kv_cache_index 섹션에서 구성합니다.
KV 캐시 인덱스 통계 조회¶
인덱스 크기, 이벤트 소스 연결 상태, 라우팅 결정 카운트를 포함한 전체 KV 캐시 인덱스 통계를 반환합니다.
응답¶
{
"enabled": true,
"config": {
"backend": "memory",
"max_entries": 100000,
"entry_ttl_seconds": 600,
"event_sources_count": 2,
"scoring": {
"overlap_weight": 0.6,
"load_weight": 0.3,
"health_weight": 0.1,
"min_overlap_threshold": 0.3
}
},
"index": {
"prefix_count": 45,
"entry_count": 120,
"total_hits": 3842,
"total_evictions": 12
},
"event_sources": [
{
"backend_name": "vllm-1",
"connected": true,
"events_received": 2100,
"events_dropped": 0,
"last_event_at": "2025-03-12T10:45:00Z",
"reconnect_count": 0
}
],
"routing_decisions": {
"kv_aware": 980,
"fallback": 120,
"total": 1100
},
"query_latency_count": 1100,
"overlap_score_count": 980
}
KV 캐시 인덱스가 비활성화된 경우(kv_cache_index.enabled: false 또는 섹션이 없는 경우), enabled는 false, config는 null, 모든 카운터는 0입니다.
응답 필드¶
| 필드 | 타입 | 설명 |
|---|---|---|
enabled |
boolean | KV 캐시 인덱스 활성화 여부 |
config |
object 또는 null | 활성 설정 요약; 비활성화 시 null |
config.backend |
string | 인덱스 백엔드: "memory" 또는 "redis" |
config.max_entries |
integer | 최대 추적 접두사 해시 항목 수 |
config.entry_ttl_seconds |
integer | 인덱스 항목 TTL (초) |
config.event_sources_count |
integer | 구성된 이벤트 소스 수 |
config.scoring |
object | 스코어링 가중치 설정 |
index.prefix_count |
integer | 추적 중인 고유 접두사 해시 수 |
index.entry_count |
integer | 총 (접두사, 백엔드) 쌍 추적 수 |
index.total_hits |
integer | 시작 이후 총 캐시 히트 기록 수 |
index.total_evictions |
integer | 시작 이후 총 캐시 제거 기록 수 |
event_sources |
array | 각 이벤트 소스 컨슈머의 상태 |
event_sources[].connected |
boolean | 컨슈머가 현재 연결되어 있는지 여부 |
event_sources[].events_received |
integer | 이 소스에서 수신한 총 이벤트 수 |
event_sources[].events_dropped |
integer | 백프레셔로 인해 드롭된 이벤트 수 |
event_sources[].reconnect_count |
integer | 시작 이후 재연결 시도 횟수 |
routing_decisions.kv_aware |
integer | KV 인식 선택을 사용하여 라우팅된 요청 수 |
routing_decisions.fallback |
integer | 기본 전략으로 폴백된 요청 수 |
routing_decisions.total |
integer | 총 라우팅 결정 수 |
예제¶
백엔드별 KV 캐시 상태 조회¶
백엔드별 KV 캐시 이벤트 통계를 반환합니다. 수신, 처리, 드롭된 이벤트, 연결 상태, 인덱스 이벤트 카운트를 포함합니다.
응답 (활성화)¶
{
"enabled": true,
"backends": [
{
"backend_name": "vllm-1",
"connection": {
"connected": true,
"reconnect_count": 0,
"last_event_at": "2025-03-12T10:45:00Z"
},
"events": {
"received": 2100,
"dropped": 0,
"index_created": 1950,
"index_evicted": 150
}
},
{
"backend_name": "vllm-2",
"connection": {
"connected": false,
"reconnect_count": 3,
"last_event_at": null
},
"events": {
"received": 0,
"dropped": 0,
"index_created": 0,
"index_evicted": 0
},
"configured_endpoint": "ws://vllm-2:8000/v1/kv_events"
}
]
}
kv_cache_index.event_sources에 나타나지만 아직 활성 컨슈머가 없는 백엔드는 connected: false와 configured_endpoint 필드와 함께 포함됩니다.
응답 (비활성화)¶
응답 필드¶
| 필드 | 타입 | 설명 |
|---|---|---|
enabled |
boolean | KV 캐시 인덱스 활성화 여부 |
backends[].backend_name |
string | 백엔드 식별자 |
backends[].connection.connected |
boolean | 이벤트 스트림 컨슈머 연결 여부 |
backends[].connection.reconnect_count |
integer | 시작 이후 재연결 시도 횟수 |
backends[].connection.last_event_at |
string 또는 null | 가장 최근 이벤트의 ISO 8601 타임스탬프 |
backends[].events.received |
integer | 이 백엔드에서 수신한 총 이벤트 수 |
backends[].events.dropped |
integer | 백프레셔로 인해 드롭된 이벤트 수 |
backends[].events.index_created |
integer | 이벤트에서 생성된 인덱스 항목 수 |
backends[].events.index_evicted |
integer | 이벤트에서 제거된 인덱스 항목 수 |
backends[].configured_endpoint |
string | 구성된 엔드포인트 URL (비활성 소스에만 존재) |
예제¶
curl -s http://localhost:8080/admin/kv-index/backends \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
KV 캐시 인덱스 삭제¶
KV 캐시 인덱스의 모든 항목을 삭제합니다. 디버깅 및 테스트용입니다. 프로덕션에서는 수신되는 KV 이벤트로부터 인덱스가 자동으로 재구축됩니다.
응답 (성공)¶
entries_before_clear는 삭제 전 총 (접두사, 백엔드) 쌍 수입니다. cleared_entries는 제거된 접두사 해시 버킷 수입니다. Redis 백엔드의 경우, cleared_entries는 삭제된 Redis 키 수를 카운트합니다; 각 키에 TTL이 있으므로 남은 키는 자동으로 만료됩니다.
응답 (비활성화)¶
예제¶
스마트 라우팅 Admin API¶
스마트 라우팅 Admin API는 모델 티어 레지스트리를 노출하며, 라우터가 각 모델에 할당하는 티어와 도메인 프로필을 조회하고 재시작 없이 런타임에서 업데이트할 수 있습니다. 모든 엔드포인트는 /admin/smart-routing 아래에 마운트되며 동일한 인증을 요구합니다.
스마트 라우팅은 YAML 설정에서 smart_routing.enabled: true로 활성화합니다. 비활성화 상태에서도 목록 엔드포인트는 "enabled": false를 반환하며 빈 프로필 목록을 돌려줍니다.
모델 프로필 목록 조회¶
명시적으로 설정된 프로필과 시작 이후 캐시된 자동 추론 프로필을 모두 반환합니다.
응답¶
{
"enabled": true,
"default_tier": 2,
"total": 2,
"profiles": [
{
"model_id": "gpt-4o",
"tier": 1,
"tier_name": "flagship",
"domains": ["general", "code", "reasoning"],
"cost_per_1k_input_tokens": 0.005,
"cost_per_1k_output_tokens": 0.015,
"source": "explicit_exact"
},
{
"model_id": "llama-3-8b-q4_K_M",
"tier": 3,
"tier_name": "lightweight",
"domains": ["general"],
"cost_per_1k_input_tokens": null,
"cost_per_1k_output_tokens": null,
"source": "explicit_pattern"
}
]
}
응답 필드¶
| 필드 | 타입 | 설명 |
|---|---|---|
enabled |
boolean | 스마트 라우팅 활성화 여부 |
default_tier |
integer | 프로필 매칭 실패 시 사용하는 기본 티어 (1, 2, 또는 3) |
total |
integer | 반환된 프로필 수 |
profiles[].model_id |
string | 모델 식별자 |
profiles[].tier |
integer | 숫자 티어: 1 = Flagship, 2 = Standard, 3 = Lightweight |
profiles[].tier_name |
string | 사람이 읽을 수 있는 티어 이름 |
profiles[].domains |
문자열 배열 | 도메인 전문화 태그 |
profiles[].cost_per_1k_input_tokens |
number 또는 null | 입력 토큰 1,000개당 비용 |
profiles[].cost_per_1k_output_tokens |
number 또는 null | 출력 토큰 1,000개당 비용 |
profiles[].source |
string | 프로필 해석 방법 |
source 값:
| 값 | 의미 |
|---|---|
explicit_exact |
정확한 모델 이름으로 설정된 프로필 |
explicit_pattern |
글로브 패턴으로 매칭된 프로필 |
auto_inferred |
가격, 기능, 이름 휴리스틱으로 추론된 프로필 |
default |
매칭 없음; 기본 티어 사용 |
예제¶
curl http://localhost:8080/admin/smart-routing/model-profiles \
-H "Authorization: Bearer $ADMIN_TOKEN"
모델 프로필 단건 조회¶
특정 모델의 해석된 프로필을 반환합니다. model-metadata.yaml에 메타데이터가 있으면 가격 및 기능 정보를 사용해 자동 추론하고, 없으면 이름 휴리스틱을 적용합니다.
경로 파라미터¶
| 파라미터 | 설명 |
|---|---|
model |
모델 식별자 (최대 256자) |
응답¶
{
"model_id": "gemini-1.5-flash",
"tier": 3,
"tier_name": "lightweight",
"domains": ["general"],
"cost_per_1k_input_tokens": null,
"cost_per_1k_output_tokens": null,
"source": "auto_inferred"
}
예제¶
curl http://localhost:8080/admin/smart-routing/model-profiles/gpt-4o \
-H "Authorization: Bearer $ADMIN_TOKEN"
모델 프로필 업데이트¶
모든 모델 프로필 설정을 교체합니다. 레지스트리는 즉시 리로드되며 추론 캐시가 초기화되어 이후 요청은 새 프로필을 기준으로 재평가됩니다.
요청 본문¶
{
"default_tier": 2,
"model_profiles": [
{
"model": "gpt-4o",
"tier": 1,
"domains": ["general", "code", "reasoning"],
"cost_per_1k_input_tokens": 0.005,
"cost_per_1k_output_tokens": 0.015
},
{
"model_pattern": "*-q4_K_M",
"tier": 3,
"domains": ["general"]
}
]
}
각 항목은 model(정확한 이름) 또는 model_pattern(글로브) 중 하나를 포함해야 합니다. 둘 다 없으면 400 Bad Request를 반환합니다.
요청 필드¶
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
model_profiles |
배열 | 예 | 프로필 목록; 기존 설정을 교체 |
model_profiles[].model |
string | 조건부 | 정확한 모델 이름 (최대 200자) |
model_profiles[].model_pattern |
string | 조건부 | *-q4_K_M 형태의 글로브 패턴 (최대 200자) |
model_profiles[].tier |
integer | 예 | 1 (Flagship), 2 (Standard), 3 (Lightweight) |
model_profiles[].domains |
문자열 배열 | 아니오 | general, code, reasoning, creative, multilingual, vision |
model_profiles[].cost_per_1k_input_tokens |
number | 아니오 | 입력 토큰 1,000개당 비용 |
model_profiles[].cost_per_1k_output_tokens |
number | 아니오 | 출력 토큰 1,000개당 비용 |
default_tier |
integer | 아니오 | 프로필 매칭 실패 시 폴백 티어 |
응답¶
예제¶
curl -X PUT http://localhost:8080/admin/smart-routing/model-profiles \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model_profiles": [
{"model": "gpt-4o", "tier": 1, "domains": ["general", "code"]},
{"model_pattern": "*-mini", "tier": 3, "domains": ["general"]}
]
}'
스마트 라우팅 상태¶
활성화 상태, 부하 상태, 분류기 방식, 정책 수 등 스마트 라우팅 전반의 상태를 반환합니다.
응답¶
{
"enabled": true,
"virtual_model": "auto",
"intercept_all": false,
"default_tier": 2,
"classifier_method": "rule",
"has_llm_classifier": false,
"load_state": "normal",
"load_monitoring_enabled": false,
"debug_headers": false,
"policy_count": 5,
"profile_count": 3
}
스마트 라우팅 통계¶
프로필 수, 정책 수, LLM 분류기 캐시 정보 등 집계된 라우팅 통계를 반환합니다.
분류 (진단용)¶
실제 라우팅 없이 요청을 분류합니다. 분류 동작을 디버깅할 때 유용합니다.
요청¶
응답¶
{
"complexity": "trivial",
"domain": "general",
"confidence": 0.95,
"classifier_type": "rule_based",
"required_capabilities": [],
"reasoning": null,
"signals": [
{"name": "message_length", "strength": 0.1, "influences": "complexity"}
]
}
시뮬레이션 (진단용)¶
실제 요청 전달 없이 전체 라우팅 파이프라인(분류 + 정책 평가 + 모델 선택 + 부하 상태)을 시뮬레이션합니다. 전체 라우팅 결정 과정을 반환합니다.
요청¶
분류 엔드포인트와 동일합니다.
응답¶
{
"routed": true,
"target_model": "gpt-4o-mini",
"classification": {
"complexity": "simple",
"domain": "general",
"confidence": 0.92,
"classifier_type": "rule_based"
},
"policy": {
"name": "trivial_to_lightweight",
"tier": 3,
"prefer_domains": [],
"require_capabilities": []
},
"load_state": "normal",
"classification_duration_ms": 0.05,
"available_models": 5
}
라우팅 정책 목록 조회¶
현재 활성화된 라우팅 정책과 해당 조건 및 대상을 반환합니다.
라우팅 정책 업데이트¶
런타임에 라우팅 정책을 핫 리로드합니다.
요청¶
{
"routing_policies": [
{
"name": "all_to_flagship",
"when": {},
"route_to": {"tier": 1}
}
],
"virtual_model": "auto",
"intercept_all": false
}
부하 상태¶
평가 정보를 포함한 현재 부하 상태를 반환합니다.
응답¶
{
"enabled": true,
"state": "normal",
"max_tier": null,
"prefer_quantized": false,
"reject_expert": false
}
캐시 통계¶
LLM 분류기 캐시 통계를 반환합니다.
응답¶
캐시 초기화¶
LLM 분류기 캐시의 모든 항목을 삭제합니다.
응답¶
가드레일 Admin API¶
가드레일 Admin API는 콘텐츠 안전 가드레일 정책을 재시작 없이 런타임에서 조회하고 조정할 수 있게 합니다. 변경 사항은 실행 중인 GuardrailService가 구독하는 핫 리로드 설정 채널을 통해 전파되므로, 모드 전환, 활성화 토글, 임곗값 변경, 라우트 오버라이드가 라이브 요청 경로에 즉시 반영됩니다. 모든 엔드포인트는 /admin/guardrails 아래에 마운트되며 나머지 Admin API와 동일한 인증 및 감사 로깅을 요구합니다.
가드레일 프로바이더 집합 자체는 설정 파일에서 정의합니다. 이 엔드포인트들은 기존 프로바이더와 글로벌/라우트별 정책을 토글하고 조정할 뿐, 프로바이더를 생성하거나 제거하지 않습니다.
가드레일 정책 조회¶
현재 적용 중인 가드레일 정책과 상태 요약을 반환합니다. 비밀값(bypass_api_keys 목록)은 마스킹됩니다. 시작 시 가드레일이 비활성화되어 실행 중인 서비스가 없으면 service_active는 false이며, 이 경우 반환되는 policy는 설정된 정책이지만 어떤 검사도 실행되지 않습니다.
응답¶
{
"enabled": true,
"mode": "enforce",
"service_active": true,
"registered_providers": ["openai-moderation", "llama-guard"],
"policy": {
"enabled": true,
"mode": "enforce",
"timeout_ms": 2000,
"on_error": "fail_open",
"block_behavior": "content_filter",
"streaming_mode": "buffer_full",
"providers": [ ... ],
"routes": { ... },
"bypass_api_keys": ["su...(24 chars)"],
"allow": { "exact": [], "regex": [] },
"deny": { "exact": [], "regex": [] }
}
}
예제¶
가드레일 정책 업데이트¶
글로벌 가드레일 정책을 부분 업데이트합니다. 모든 필드는 선택 사항이며, 제공된 필드만 변경됩니다. 프로바이더와 라우트별 오버라이드는 아래의 전용 엔드포인트로 관리합니다. 후보 정책은 적용 전에 검증되며, 잘못된 변경(예: timeout_ms: 0, 또는 프로바이더 없이 enforce 모드 활성화)은 400을 반환하고 실행 중인 정책을 그대로 둡니다.
요청 본문¶
| 필드 | 타입 | 설명 |
|---|---|---|
enabled |
boolean | 가드레일을 글로벌하게 켜고 끔 |
mode |
string | monitor 또는 enforce |
timeout_ms |
integer | 글로벌 가드레일 타임아웃(밀리초) |
on_error |
string | fail_open 또는 fail_closed |
block_behavior |
string | content_filter, error, 또는 refusal_message |
streaming_mode |
string | buffer_full, chunked, 또는 passthrough |
allow |
object | 글로벌 허용 목록 교체({ "exact": [], "regex": [] }) |
deny |
object | 글로벌 차단 목록 교체 |
bypass_api_keys |
array | 우회 API 키 목록 교체 |
응답¶
예제¶
curl -X PATCH http://localhost:8080/admin/guardrails \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"mode": "enforce"}'
가드레일 프로바이더 업데이트¶
설정된 단일 프로바이더의 런타임 설정을 업데이트합니다. 모든 필드는 선택 사항입니다. 주어진 이름의 프로바이더가 설정되어 있지 않으면 404를 반환합니다.
요청 본문¶
| 필드 | 타입 | 설명 |
|---|---|---|
enabled |
boolean | 이 프로바이더를 활성화 또는 비활성화 |
category_thresholds |
object | 카테고리별 점수 임곗값 교체({ "violence": 0.8 }) |
timeout_ms |
integer 또는 null | 프로바이더별 타임아웃 오버라이드 설정 또는 해제 |
on_error |
string 또는 null | 프로바이더별 오류 정책 오버라이드 설정 또는 해제 |
응답¶
예제¶
curl -X PUT http://localhost:8080/admin/guardrails/providers/llama-guard \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
가드레일 라우트 오버라이드 설정¶
주어진 라우트에 대한 라우트별 가드레일 오버라이드를 생성하거나 교체합니다. 요청 본문은 라우트 오버라이드 객체이며, 생략된 필드는 글로벌 정책을 상속합니다.
요청 본문¶
| 필드 | 타입 | 설명 |
|---|---|---|
mode |
string | 이 라우트의 동작 모드 오버라이드 |
enabled |
boolean | 이 라우트에서 가드레일 실행 여부 오버라이드 |
providers |
array | 이 라우트를 프로바이더 이름의 부분 집합으로 제한 |
category_thresholds |
object | 라우트별 카테고리 임곗값 |
allow |
object | 라우트 전용 허용 목록 |
deny |
object | 라우트 전용 차단 목록 |
응답¶
예제¶
curl -X PUT http://localhost:8080/admin/guardrails/routes/gpt-4o \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"mode": "monitor"}'
가드레일 라우트 오버라이드 삭제¶
라우트별 오버라이드를 제거하여 해당 라우트를 글로벌 정책으로 되돌립니다. 해당 라우트에 오버라이드가 설정되어 있지 않으면 404를 반환합니다.
응답¶
가드레일 테스트 (드라이런)¶
임곗값 튜닝을 위한 진단 엔드포인트입니다. 등록된 모든 프로바이더를 제공된 샘플 텍스트에 대해 실행하고, 각 프로바이더의 판정과 집계된 최고 심각도 우선(most-severe-wins) 판정을 반환합니다. 드라이런은 글로벌 모드와 우회 목록을 무시하여 가공되지 않은 프로바이더 출력을 노출합니다. 비활성화된 프로바이더(및 요청된 단계에 적용되지 않는 프로바이더)는 건너뜀(skipped)으로 보고됩니다. 활성화된 가드레일 서비스가 없으면 400을 반환합니다.
요청 본문¶
| 필드 | 타입 | 설명 |
|---|---|---|
text |
string | 평가할 샘플 텍스트(필수) |
stage |
string | input(기본값) 또는 output |
model |
string | 평가 컨텍스트용 선택적 모델 식별자 |
route |
string | 평가 컨텍스트용 선택적 라우트 이름 |
응답¶
{
"stage": "input",
"providers": [
{
"provider": "openai-moderation",
"skipped": false,
"verdict": { "verdict": "allow" }
},
{
"provider": "llama-guard",
"skipped": false,
"verdict": {
"verdict": "block",
"category": "violence",
"score": 0.97,
"reason": "..."
}
}
],
"aggregated": {
"verdict": "block",
"category": "violence",
"score": 0.97,
"reason": "..."
}
}
예제¶
curl -X POST http://localhost:8080/admin/guardrails/test \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"text": "sample prompt to evaluate", "stage": "input"}'
데이터 모델¶
설정 섹션¶
| 섹션 | 설명 | 핫 리로드 |
|---|---|---|
server |
바인드 주소, 워커, 연결 풀 | 재시작 필요 |
backends |
백엔드 URL, 가중치, 모델 | 점진적 |
health_checks |
간격, 임계값 | 점진적 |
logging |
로그 레벨, 형식, 출력 | 즉시 |
retry |
최대 시도, 지연, 백오프 | 즉시 |
timeouts |
연결, 요청, 유휴 타임아웃 | 점진적 |
rate_limiting |
제한, 스토리지, 화이트리스트 | 즉시 |
circuit_breaker |
임계값, 복구 시간 | 즉시 |
global_prompts |
시스템 프롬프트 주입 | 즉시 |
fallback |
폴백 체인, 정책 | 점진적 |
files |
Files API 설정 | 점진적 |
api_keys |
API 키 설정 | 즉시 |
metrics |
Prometheus, 레이블 | 점진적 |
admin |
Admin API 설정 | 점진적 |
admin.stats |
통계 수집 설정 | 즉시 |
routing |
모델 라우팅 규칙 | 점진적 |
smart_routing |
모델 티어 레지스트리 및 프로필 | 즉시 |
백엔드 객체¶
{
"name": "string",
"url": "string (http:// 또는 https://)",
"api_key": "string (선택 사항, 응답에서 마스킹)",
"weight": "integer (1-100)",
"models": ["string"],
"enabled": "boolean",
"health_check": {
"enabled": "boolean",
"path": "string",
"interval": "string (duration)"
}
}
히스토리 항목 객체¶
{
"version": "integer",
"timestamp": "string (ISO 8601)",
"sections_changed": ["string"],
"source": "string (api|file_reload|initial|rollback)",
"user": "string",
"description": "string (선택 사항)",
"rollback_available": "boolean"
}
검증 결과 객체¶
{
"valid": "boolean",
"errors": [
{
"field": "string",
"message": "string",
"code": "string"
}
],
"warnings": [
{
"field": "string",
"message": "string"
}
]
}
핫 리로드 동작¶
업데이트 타입¶
| 타입 | 동작 | 섹션 |
|---|---|---|
| 즉시 | 즉시 적용, 중단 없음 | logging, rate_limiting, circuit_breaker, retry, global_prompts, api_keys |
| 점진적 | 기존 연결 유지, 새 연결이 새 설정 사용 | backends, health_checks, timeouts, fallback, files, metrics, admin, routing |
| 재시작 필요 | 경고로 로깅, 서버 재시작 필요 | server.bind_address, server.workers |
예제 워크플로우¶
# 1. 현재 설정 확인
curl -s http://localhost:8080/admin/config/logging | jq
# 2. 변경 검증
curl -X POST http://localhost:8080/admin/config/validate \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"section": "logging", "config": {"level": "debug"}}'
# 3. 변경 적용 (즉시 효과)
curl -X PATCH http://localhost:8080/admin/config/logging \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"config": {"level": "debug"}}'
# 4. 변경 확인
curl -s http://localhost:8080/admin/config/logging | jq '.config.level'
오류 처리¶
오류 응답 형식¶
오류 코드¶
| 코드 | HTTP 상태 | 설명 |
|---|---|---|
VALIDATION_ERROR |
400 | 설정 검증 실패 |
INVALID_SECTION |
400 | 알 수 없는 설정 섹션 |
PARSE_ERROR |
400 | 설정 콘텐츠 파싱 실패 |
SECTION_NOT_FOUND |
404 | 섹션을 찾을 수 없음 |
VERSION_NOT_FOUND |
404 | 히스토리 버전을 찾을 수 없음 |
BACKEND_NOT_FOUND |
404 | 백엔드를 찾을 수 없음 |
BACKEND_EXISTS |
409 | 해당 이름의 백엔드가 이미 존재함 |
CONTENT_TOO_LARGE |
413 | 설정 콘텐츠가 1MB 제한 초과 |
INTERNAL_ERROR |
500 | 내부 서버 오류 |
오류 예제¶
// 검증 오류
{
"error_code": "VALIDATION_ERROR",
"message": "Configuration validation failed",
"details": {
"errors": [
{"field": "workers", "message": "workers must be greater than 0"}
]
}
}
// 섹션을 찾을 수 없음
{
"error_code": "SECTION_NOT_FOUND",
"message": "Configuration section 'invalid' not found",
"details": {
"available_sections": ["server", "backends", "logging", "..."]
}
}
// 백엔드가 이미 존재함
{
"error_code": "BACKEND_EXISTS",
"message": "Backend 'openai' already exists",
"details": {
"existing_backend": "openai"
}
}
클라이언트 SDK 예제¶
Python¶
import requests
from typing import Optional, Dict, Any, List
from dataclasses import dataclass
@dataclass
class ContinuumAdminClient:
"""Continuum Router Admin API 클라이언트"""
base_url: str
token: str
def __post_init__(self):
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
})
# 설정 쿼리 API
def get_full_config(self) -> Dict[str, Any]:
"""마스킹된 민감 데이터가 포함된 전체 설정 가져오기"""
resp = self.session.get(f"{self.base_url}/admin/config/full")
resp.raise_for_status()
return resp.json()
def get_sections(self) -> List[Dict[str, Any]]:
"""모든 설정 섹션 가져오기"""
resp = self.session.get(f"{self.base_url}/admin/config/sections")
resp.raise_for_status()
return resp.json()["sections"]
def get_section(self, section: str) -> Dict[str, Any]:
"""특정 섹션의 설정 가져오기"""
resp = self.session.get(f"{self.base_url}/admin/config/{section}")
resp.raise_for_status()
return resp.json()
def get_schema(self, section: Optional[str] = None) -> Dict[str, Any]:
"""검증을 위한 JSON 스키마 가져오기"""
params = {"section": section} if section else {}
resp = self.session.get(
f"{self.base_url}/admin/config/schema",
params=params
)
resp.raise_for_status()
return resp.json()
# 설정 수정 API
def update_section(self, section: str, config: Dict[str, Any]) -> Dict[str, Any]:
"""섹션 설정 대체"""
resp = self.session.put(
f"{self.base_url}/admin/config/{section}",
json={"config": config}
)
resp.raise_for_status()
return resp.json()
def patch_section(self, section: str, config: Dict[str, Any]) -> Dict[str, Any]:
"""섹션 설정 부분 업데이트"""
resp = self.session.patch(
f"{self.base_url}/admin/config/{section}",
json={"config": config}
)
resp.raise_for_status()
return resp.json()
def validate_config(
self,
section: str,
config: Dict[str, Any],
dry_run: bool = True
) -> Dict[str, Any]:
"""적용 없이 설정 검증"""
resp = self.session.post(
f"{self.base_url}/admin/config/validate",
json={"section": section, "config": config, "dry_run": dry_run}
)
resp.raise_for_status()
return resp.json()
def apply_config(
self,
sections: Optional[List[str]] = None,
force: bool = False
) -> Dict[str, Any]:
"""보류 중인 설정 변경 적용"""
body = {"force": force}
if sections:
body["sections"] = sections
resp = self.session.post(
f"{self.base_url}/admin/config/apply",
json=body
)
resp.raise_for_status()
return resp.json()
# 설정 저장/복원 API
def export_config(
self,
format: str = "yaml",
sections: Optional[List[str]] = None,
include_sensitive: bool = False
) -> str:
"""지정된 형식으로 설정 내보내기"""
body = {"format": format, "include_sensitive": include_sensitive}
if sections:
body["sections"] = sections
resp = self.session.post(
f"{self.base_url}/admin/config/export",
json=body
)
resp.raise_for_status()
return resp.json()["content"]
def import_config(
self,
content: str,
format: str = "yaml",
apply: bool = True,
dry_run: bool = False
) -> Dict[str, Any]:
"""콘텐츠에서 설정 가져오기"""
resp = self.session.post(
f"{self.base_url}/admin/config/import",
json={
"format": format,
"content": content,
"apply": apply,
"dry_run": dry_run
}
)
resp.raise_for_status()
return resp.json()
def get_history(
self,
limit: int = 20,
offset: int = 0,
section: Optional[str] = None
) -> Dict[str, Any]:
"""설정 변경 히스토리 가져오기"""
params = {"limit": limit, "offset": offset}
if section:
params["section"] = section
resp = self.session.get(
f"{self.base_url}/admin/config/history",
params=params
)
resp.raise_for_status()
return resp.json()
def rollback(
self,
version: int,
sections: Optional[List[str]] = None,
dry_run: bool = False
) -> Dict[str, Any]:
"""이전 버전으로 롤백"""
body = {"dry_run": dry_run}
if sections:
body["sections"] = sections
resp = self.session.post(
f"{self.base_url}/admin/config/rollback/{version}",
json=body
)
resp.raise_for_status()
return resp.json()
# 백엔드 관리 API
def list_backends(self) -> List[Dict[str, Any]]:
"""모든 백엔드 목록"""
resp = self.session.get(f"{self.base_url}/admin/backends")
resp.raise_for_status()
return resp.json()["backends"]
def get_backend(self, name: str) -> Dict[str, Any]:
"""백엔드 설정 가져오기"""
resp = self.session.get(f"{self.base_url}/admin/backends/{name}")
resp.raise_for_status()
return resp.json()
def add_backend(
self,
name: str,
url: str,
weight: int = 1,
models: Optional[List[str]] = None
) -> Dict[str, Any]:
"""새 백엔드 추가"""
body = {"name": name, "url": url, "weight": weight}
if models:
body["models"] = models
resp = self.session.post(
f"{self.base_url}/admin/backends",
json=body
)
resp.raise_for_status()
return resp.json()
def update_backend(self, name: str, **kwargs) -> Dict[str, Any]:
"""백엔드 설정 업데이트"""
resp = self.session.put(
f"{self.base_url}/admin/backends/{name}",
json=kwargs
)
resp.raise_for_status()
return resp.json()
def delete_backend(self, name: str, force: bool = False) -> Dict[str, Any]:
"""백엔드 삭제"""
params = {"force": str(force).lower()} if force else {}
resp = self.session.delete(
f"{self.base_url}/admin/backends/{name}",
params=params
)
resp.raise_for_status()
return resp.json()
def update_backend_weight(self, name: str, weight: int) -> Dict[str, Any]:
"""백엔드 가중치 업데이트"""
resp = self.session.put(
f"{self.base_url}/admin/backends/{name}/weight",
json={"weight": weight}
)
resp.raise_for_status()
return resp.json()
def update_backend_models(
self,
name: str,
models: List[str],
append: bool = False
) -> Dict[str, Any]:
"""백엔드 모델 업데이트"""
resp = self.session.put(
f"{self.base_url}/admin/backends/{name}/models",
json={"models": models, "append": append}
)
resp.raise_for_status()
return resp.json()
# 사용 예제
if __name__ == "__main__":
client = ContinuumAdminClient(
base_url="http://localhost:8080",
token="your-admin-token"
)
# 현재 로깅 설정 가져오기
logging_config = client.get_section("logging")
print(f"현재 로그 레벨: {logging_config['config']['level']}")
# 로깅 레벨 업데이트
result = client.patch_section("logging", {"level": "debug"})
print(f"업데이트됨: {result['success']}")
# 새 백엔드 추가
client.add_backend(
name="new-ollama",
url="http://192.168.1.100:11434",
weight=2,
models=["llama3.2", "mistral"]
)
# 설정 백업 내보내기
backup = client.export_config(format="yaml")
with open("config-backup.yaml", "w") as f:
f.write(backup)
JavaScript/TypeScript¶
interface ConfigSection {
name: string;
config: Record<string, any>;
hot_reload_capability: 'immediate' | 'gradual' | 'requires_restart';
}
interface HistoryEntry {
version: number;
timestamp: string;
sections_changed: string[];
source: string;
user: string;
}
interface Backend {
name: string;
url: string;
weight: number;
models: string[];
enabled: boolean;
health_status: string;
}
class ContinuumAdminClient {
private baseUrl: string;
private token: string;
constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl;
this.token = token;
}
private async request<T>(
method: string,
path: string,
body?: any,
params?: Record<string, string>
): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`);
if (params) {
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
}
const response = await fetch(url.toString(), {
method,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// 설정 쿼리 API
async getFullConfig(): Promise<any> {
return this.request('GET', '/admin/config/full');
}
async getSections(): Promise<ConfigSection[]> {
const result = await this.request<{ sections: ConfigSection[] }>(
'GET', '/admin/config/sections'
);
return result.sections;
}
async getSection(section: string): Promise<ConfigSection> {
return this.request('GET', `/admin/config/${section}`);
}
async getSchema(section?: string): Promise<any> {
const params = section ? { section } : undefined;
return this.request('GET', '/admin/config/schema', undefined, params);
}
// 설정 수정 API
async updateSection(section: string, config: Record<string, any>): Promise<any> {
return this.request('PUT', `/admin/config/${section}`, { config });
}
async patchSection(section: string, config: Record<string, any>): Promise<any> {
return this.request('PATCH', `/admin/config/${section}`, { config });
}
async validateConfig(
section: string,
config: Record<string, any>,
dryRun: boolean = true
): Promise<any> {
return this.request('POST', '/admin/config/validate', {
section,
config,
dry_run: dryRun,
});
}
async applyConfig(sections?: string[], force: boolean = false): Promise<any> {
return this.request('POST', '/admin/config/apply', { sections, force });
}
// 설정 저장/복원 API
async exportConfig(
format: 'yaml' | 'json' | 'toml' = 'yaml',
sections?: string[],
includeSensitive: boolean = false
): Promise<string> {
const result = await this.request<{ content: string }>(
'POST', '/admin/config/export',
{ format, sections, include_sensitive: includeSensitive }
);
return result.content;
}
async importConfig(
content: string,
format: 'yaml' | 'json' | 'toml' = 'yaml',
apply: boolean = true,
dryRun: boolean = false
): Promise<any> {
return this.request('POST', '/admin/config/import', {
format,
content,
apply,
dry_run: dryRun,
});
}
async getHistory(
limit: number = 20,
offset: number = 0,
section?: string
): Promise<{ history: HistoryEntry[]; total_entries: number }> {
const params: Record<string, string> = {
limit: limit.toString(),
offset: offset.toString(),
};
if (section) params.section = section;
return this.request('GET', '/admin/config/history', undefined, params);
}
async rollback(
version: number,
sections?: string[],
dryRun: boolean = false
): Promise<any> {
return this.request('POST', `/admin/config/rollback/${version}`, {
sections,
dry_run: dryRun,
});
}
// 백엔드 관리 API
async listBackends(): Promise<Backend[]> {
const result = await this.request<{ backends: Backend[] }>(
'GET', '/admin/backends'
);
return result.backends;
}
async getBackend(name: string): Promise<Backend> {
return this.request('GET', `/admin/backends/${name}`);
}
async addBackend(
name: string,
url: string,
weight: number = 1,
models?: string[]
): Promise<any> {
return this.request('POST', '/admin/backends', {
name,
url,
weight,
models,
});
}
async updateBackend(name: string, updates: Partial<Backend>): Promise<any> {
return this.request('PUT', `/admin/backends/${name}`, updates);
}
async deleteBackend(name: string, force: boolean = false): Promise<any> {
const params = force ? { force: 'true' } : undefined;
return this.request('DELETE', `/admin/backends/${name}`, undefined, params);
}
async updateBackendWeight(name: string, weight: number): Promise<any> {
return this.request('PUT', `/admin/backends/${name}/weight`, { weight });
}
async updateBackendModels(
name: string,
models: string[],
append: boolean = false
): Promise<any> {
return this.request('PUT', `/admin/backends/${name}/models`, {
models,
append,
});
}
}
// 사용 예제
async function main() {
const client = new ContinuumAdminClient(
'http://localhost:8080',
'your-admin-token'
);
// 현재 로깅 설정 가져오기
const loggingConfig = await client.getSection('logging');
console.log(`현재 로그 레벨: ${loggingConfig.config.level}`);
// 로깅 레벨 업데이트
const result = await client.patchSection('logging', { level: 'debug' });
console.log(`업데이트됨: ${result.success}`);
// 새 백엔드 추가
await client.addBackend('new-ollama', 'http://192.168.1.100:11434', 2, [
'llama3.2',
'mistral',
]);
// 설정 백업 내보내기
const backup = await client.exportConfig('yaml');
console.log('설정 내보내기 완료');
}
main().catch(console.error);
Go¶
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
type ContinuumAdminClient struct {
BaseURL string
Token string
client *http.Client
}
func NewClient(baseURL, token string) *ContinuumAdminClient {
return &ContinuumAdminClient{
BaseURL: baseURL,
Token: token,
client: &http.Client{},
}
}
func (c *ContinuumAdminClient) request(method, path string, body interface{}) (map[string]interface{}, error) {
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewBuffer(jsonBody)
}
req, err := http.NewRequest(method, c.BaseURL+path, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("HTTP %d: %v", resp.StatusCode, result)
}
return result, nil
}
// GetFullConfig 전체 설정을 조회합니다
func (c *ContinuumAdminClient) GetFullConfig() (map[string]interface{}, error) {
return c.request("GET", "/admin/config/full", nil)
}
// GetSection 특정 설정 섹션을 조회합니다
func (c *ContinuumAdminClient) GetSection(section string) (map[string]interface{}, error) {
return c.request("GET", "/admin/config/"+section, nil)
}
// PatchSection 설정 섹션을 부분 업데이트합니다
func (c *ContinuumAdminClient) PatchSection(section string, config map[string]interface{}) (map[string]interface{}, error) {
return c.request("PATCH", "/admin/config/"+section, map[string]interface{}{
"config": config,
})
}
// AddBackend 새 백엔드를 추가합니다
func (c *ContinuumAdminClient) AddBackend(name, backendURL string, weight int, models []string) (map[string]interface{}, error) {
return c.request("POST", "/admin/backends", map[string]interface{}{
"name": name,
"url": backendURL,
"weight": weight,
"models": models,
})
}
// ExportConfig 지정된 형식으로 설정을 내보냅니다
func (c *ContinuumAdminClient) ExportConfig(format string) (string, error) {
result, err := c.request("POST", "/admin/config/export", map[string]interface{}{
"format": format,
})
if err != nil {
return "", err
}
return result["content"].(string), nil
}
// GetHistory 설정 변경 히스토리를 조회합니다
func (c *ContinuumAdminClient) GetHistory(limit int) (map[string]interface{}, error) {
u, _ := url.Parse(c.BaseURL + "/admin/config/history")
q := u.Query()
q.Set("limit", fmt.Sprintf("%d", limit))
u.RawQuery = q.Encode()
return c.request("GET", u.Path+"?"+u.RawQuery, nil)
}
func main() {
client := NewClient("http://localhost:8080", "your-admin-token")
// 현재 로깅 설정 가져오기
config, _ := client.GetSection("logging")
fmt.Printf("현재 설정: %v\n", config)
// 로깅 레벨 업데이트
result, _ := client.PatchSection("logging", map[string]interface{}{
"level": "debug",
})
fmt.Printf("업데이트 결과: %v\n", result)
// 새 백엔드 추가
client.AddBackend("new-ollama", "http://192.168.1.100:11434", 2, []string{"llama3.2"})
// 설정 내보내기
backup, _ := client.ExportConfig("yaml")
fmt.Println("설정 내보내기 완료")
fmt.Println(backup)
}
모범 사례¶
1. 적용 전 항상 검증¶
# 1단계: 검증
curl -X POST http://localhost:8080/admin/config/validate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"section": "logging", "config": {"level": "debug"}}'
# 2단계: 유효한 경우에만 적용
curl -X PATCH http://localhost:8080/admin/config/logging \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"config": {"level": "debug"}}'
2. 가져오기에 Dry Run 사용¶
# 가져오기 변경 미리보기
curl -X POST http://localhost:8080/admin/config/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"format": "yaml",
"content": "...",
"dry_run": true
}'
3. 정기적인 설정 백업¶
# 일일 백업 스크립트
#!/bin/bash
DATE=$(date +%Y%m%d)
curl -s -X POST http://localhost:8080/admin/config/export \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"format": "yaml"}' | jq -r '.content' > "config-backup-$DATE.yaml"
4. 설정 히스토리 모니터링¶
# 최근 변경 확인
curl -s http://localhost:8080/admin/config/history?limit=5 \
-H "Authorization: Bearer $TOKEN" | jq '.history[] | {version, timestamp, sections_changed}'
5. 최소 변경에 부분 업데이트 (PATCH) 사용¶
# 필요한 것만 업데이트
curl -X PATCH http://localhost:8080/admin/config/rate_limiting \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"config": {"requests_per_minute": 200}}'
6. 프로덕션 전 스테이징에서 설정 변경 테스트¶
# 예제: 프로덕션 전 스테이징에서 설정 테스트
staging_client = ContinuumAdminClient("http://staging:8080", staging_token)
production_client = ContinuumAdminClient("http://production:8080", prod_token)
# 먼저 스테이징에 적용
staging_client.patch_section("rate_limiting", {"requests_per_minute": 500})
# 스테이징에서 확인
staging_config = staging_client.get_section("rate_limiting")
assert staging_config["config"]["requests_per_minute"] == 500
# 그 다음 프로덕션에 적용
production_client.patch_section("rate_limiting", {"requests_per_minute": 500})
보안 고려 사항¶
1. 민감 데이터 처리¶
- 모든 API 응답은 민감한 필드 (API 키, 비밀번호, 토큰)를 자동으로 마스킹합니다
include_sensitive: true는 절대적으로 필요한 경우에만 export에서 사용하세요- 감사 로그는 민감 데이터 액세스 시 기록합니다
2. 인증 모범 사례¶
admin:
auth:
method: bearer_token
token: "${ADMIN_TOKEN}" # 환경 변수 사용
# IP로 액세스 제한
ip_whitelist:
- "10.0.0.0/8" # 내부 네트워크만
- "192.168.1.0/24" # 사무실 네트워크
3. 감사 로깅¶
모든 설정 변경은 다음과 함께 로깅됩니다:
- 타임스탬프
- 사용자/소스
- 변경된 섹션
- 이전 및 새 값 (민감 데이터 마스킹)
4. Admin 엔드포인트 속도 제한¶
남용 방지를 위해 admin 엔드포인트 속도 제한을 고려하세요:
5. 주요 변경 전 백업¶
# 주요 변경 전 항상 백업
backup=$(curl -s -X POST http://localhost:8080/admin/config/export \
-H "Authorization: Bearer $TOKEN" \
-d '{"format": "yaml"}' | jq -r '.content')
# 변경 수행...
# 필요시 복원
curl -X POST http://localhost:8080/admin/config/import \
-H "Authorization: Bearer $TOKEN" \
-d "{\"format\": \"yaml\", \"content\": $(echo "$backup" | jq -Rs .)}"
프롬프트 파일 관리 API¶
프롬프트 파일 관리 API를 사용하면 외부 Markdown 파일에 저장된 시스템 프롬프트를 관리할 수 있습니다. 메인 설정 파일을 수정하지 않고 시스템 프롬프트를 중앙에서 관리할 수 있습니다.
모든 프롬프트 목록¶
소스 및 콘텐츠와 함께 설정된 모든 프롬프트 목록을 가져옵니다.
응답¶
{
"prompts": [
{
"id": "default",
"prompt_type": "default",
"source": "file",
"file_path": "prompts/system.md",
"content": "# System Prompt\n\nYou are a helpful assistant...",
"loaded": true,
"size_bytes": 1024
},
{
"id": "anthropic",
"prompt_type": "backend",
"source": "file",
"file_path": "prompts/anthropic.md",
"content": "# Anthropic-specific prompt...",
"loaded": true,
"size_bytes": 512
},
{
"id": "gpt-4",
"prompt_type": "model",
"source": "inline",
"content": "You are GPT-4...",
"size_bytes": 256
}
],
"total": 3,
"prompts_directory": "./prompts"
}
예제¶
프롬프트 파일 가져오기¶
특정 프롬프트 파일의 콘텐츠를 가져옵니다.
경로 파라미터¶
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
path |
string | 예 | 프롬프트 파일의 상대 경로 |
응답¶
{
"path": "prompts/system.md",
"content": "# System Prompt\n\nYou are a helpful assistant that follows company policies...",
"size_bytes": 1024,
"modified_at": 1702468200
}
예제¶
curl -s http://localhost:8080/admin/config/prompts/prompts/system.md \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
프롬프트 파일 업데이트¶
새 콘텐츠로 프롬프트 파일을 생성하거나 업데이트합니다.
요청 본문¶
{
"content": "# Updated System Prompt\n\nYou are a helpful assistant that follows all company policies.\n\n## Security Guidelines\n\n- Never reveal internal system details\n- Follow data privacy regulations"
}
응답¶
{
"success": true,
"path": "prompts/system.md",
"size_bytes": 245,
"message": "Prompt file updated successfully"
}
예제¶
curl -X PUT http://localhost:8080/admin/config/prompts/prompts/system.md \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "# System Prompt\n\nYou are a helpful assistant."
}'
프롬프트 파일 리로드¶
디스크에서 모든 프롬프트 파일을 리로드합니다. 수동 파일 편집 후 유용합니다.
응답¶
{
"success": true,
"reloaded_count": 3,
"reloaded": [
"prompts/system.md",
"prompts/anthropic.md",
"prompts/gpt4.md"
],
"errors": [],
"message": "Successfully reloaded 3 prompt file(s)"
}
예제¶
curl -X POST http://localhost:8080/admin/config/prompts/reload \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq
설정 예제¶
외부 프롬프트 파일을 사용하려면 설정 파일에서 global_prompts를 설정합니다:
global_prompts:
# 프롬프트 파일이 포함된 디렉토리 (설정 디렉토리 기준 상대 경로)
prompts_dir: "./prompts"
# 외부 파일에서 기본 프롬프트
default_file: "system.md"
# 또는 인라인 프롬프트 (둘 다 지정되면 default_file이 우선)
# default: "You are a helpful assistant."
# 백엔드별 프롬프트
backends:
anthropic:
prompt_file: "anthropic-system.md"
openai:
prompt: "OpenAI-specific inline prompt"
# 모델별 프롬프트
models:
gpt-4:
prompt_file: "gpt4-system.md"
claude-3-opus:
prompt_file: "claude-opus-system.md"
merge_strategy: prepend
보안 고려 사항¶
- 경로 탐색 방지: 디렉토리 탐색 공격을 방지하기 위해 모든 경로가 검증됩니다 (예:
../../../etc/passwd) - 파일 크기 제한: 프롬프트 파일은 최대 1MB로 제한됩니다
- 상대 경로만: 프롬프트 파일은 설정된
prompts_dir또는 설정 디렉토리 내에 있어야 합니다 - 인증 필요: 모든 프롬프트 관리 엔드포인트는 admin 인증이 필요합니다
부록: 빠른 참조¶
설정 섹션¶
| 섹션 | 핫 리로드 | 설명 |
|---|---|---|
server |
재시작 | 바인드 주소, 워커 |
backends |
점진적 | 백엔드 URL, 가중치 |
health_checks |
점진적 | 헬스 모니터링 |
logging |
즉시 | 로그 레벨, 형식 |
retry |
즉시 | 재시도 정책 |
timeouts |
점진적 | 요청 타임아웃 |
rate_limiting |
즉시 | 속도 제한 |
circuit_breaker |
즉시 | 서킷 브레이커 |
global_prompts |
즉시 | 시스템 프롬프트 |
fallback |
점진적 | 모델 폴백 |
files |
점진적 | Files API |
api_keys |
즉시 | API 키 |
metrics |
점진적 | Prometheus 메트릭 |
admin |
점진적 | Admin 설정 |
admin.stats |
즉시 | 통계 수집 설정 |
routing |
점진적 | 라우팅 규칙 |
prefix_routing |
즉시 | 접두사 인식 KV 캐시 라우팅 |
response_cache |
즉시 | 응답 캐시 설정 |
kv_cache_index |
재시작 필요 | KV 캐시 인덱스 백엔드 및 이벤트 소스 |
HTTP 상태 코드¶
| 코드 | 의미 |
|---|---|
| 200 | 성공 |
| 400 | Bad Request (검증 오류) |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 413 | Payload Too Large |
| 500 | Internal Server Error |
일반적인 curl 명령어¶
# 전체 설정 가져오기
curl -s http://localhost:8080/admin/config/full -H "Authorization: Bearer $TOKEN"
# 로깅 레벨 업데이트
curl -X PATCH http://localhost:8080/admin/config/logging \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"config": {"level": "debug"}}'
# 백엔드 추가
curl -X POST http://localhost:8080/admin/backends \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name": "new", "url": "http://host:port", "weight": 1}'
# 설정 내보내기
curl -X POST http://localhost:8080/admin/config/export \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"format": "yaml"}'
# 히스토리 보기
curl -s http://localhost:8080/admin/config/history -H "Authorization: Bearer $TOKEN"
# 롤백
curl -X POST http://localhost:8080/admin/config/rollback/5 \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'
# API 키 목록 (마스킹)
curl -s http://localhost:8080/admin/api-keys -H "Authorization: Bearer $TOKEN"
# API 키 생성 (전체 값은 한 번만 반환)
curl -X POST http://localhost:8080/admin/api-keys \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"id": "key-1", "user_id": "user-1", "organization_id": "org-1", "scopes": ["read", "write"]}'
# API 키 교체
curl -X POST http://localhost:8080/admin/api-keys/key-1/rotate -H "Authorization: Bearer $TOKEN"
# API 키 비활성화 / 활성화
curl -X POST http://localhost:8080/admin/api-keys/key-1/disable -H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:8080/admin/api-keys/key-1/enable -H "Authorization: Bearer $TOKEN"
# API 키 폐기
curl -X DELETE http://localhost:8080/admin/api-keys/key-1 -H "Authorization: Bearer $TOKEN"
# API 키별·사용자별 사용량 통계
curl -s http://localhost:8080/admin/stats/api-keys -H "Authorization: Bearer $TOKEN"
curl -s http://localhost:8080/admin/stats/users -H "Authorization: Bearer $TOKEN"