콘텐츠로 이동

Admin REST API 레퍼런스

이 문서는 Continuum Router의 Admin REST API로 설정 제어 애플리케이션을 만드는 개발자를 위한 가이드입니다. 설정 관리 API를 사용하면 서버 재시작 없이 런타임에 설정을 보고, 수정하고, 관리할 수 있습니다.

목차


개요

Admin REST API는 Continuum Router의 설정 시스템에 프로그래밍 방식으로 액세스할 수 있게 하며 다음을 지원합니다:

  • 실시간 설정 보기: 민감한 데이터가 자동으로 마스킹된 현재 설정 조회
  • 동적 설정 업데이트: 서버 재시작 없이 설정 섹션 수정
  • 설정 버전 관리: 전체 히스토리 및 롤백 기능으로 변경 사항 추적
  • 백엔드 관리: 백엔드를 동적으로 추가, 제거, 수정
  • 내보내기/가져오기: 여러 형식 (YAML, JSON, TOML)으로 설정 저장 및 복원

주요 기능

기능 설명
핫 리로드 섹션 타입에 따라 변경 사항이 즉시 또는 점진적으로 적용됨
민감 정보 마스킹 API 키, 비밀번호, 토큰이 응답에서 자동으로 마스킹됨
검증 dry-run 지원으로 적용 전 모든 변경 사항 검증
감사 로깅 보안 및 규정 준수를 위해 모든 수정 사항 로깅
히스토리 추적 롤백을 위해 최대 100개의 설정 버전 유지

인증

모든 Admin API 엔드포인트는 Admin Auth 시스템을 통한 인증이 필요합니다.

인증 방법

1. Bearer 토큰

Authorization: Bearer <admin-token>
curl -H "Authorization: Bearer your-admin-token" \
  http://localhost:8080/admin/config/full

2. Basic 인증

Authorization: Basic <base64(username:password)>
curl -u admin:password http://localhost:8080/admin/config/full

3. API 키 헤더

X-API-Key: <admin-api-key>
curl -H "X-API-Key: your-admin-key" http://localhost:8080/admin/config/full

설정

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

http://localhost:8080/admin

일반 요청 헤더

Content-Type: application/json
Accept: application/json
Authorization: Bearer <token>

일반 응답 헤더

Content-Type: application/json
X-Request-Id: <unique-request-id>

설정 쿼리 API

전체 설정 가져오기

민감한 정보가 마스킹된 전체 설정을 조회합니다.

GET /admin/config/full

응답

{
  "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"
}

예제

curl -s http://localhost:8080/admin/config/full \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

설정 섹션 목록

핫 리로드 기능과 함께 사용 가능한 모든 설정 섹션을 가져옵니다.

GET /admin/config/sections

응답

{
  "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'

섹션 설정 가져오기

특정 섹션의 설정을 조회합니다.

GET /admin/config/{section}

경로 파라미터

파라미터 타입 필수 설명
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를 조회합니다.

GET /admin/config/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

섹션 설정 대체

전체 섹션 설정을 새 값으로 대체합니다.

PUT /admin/config/{section}

요청 본문

{
  "config": {
    "level": "debug",
    "format": "json"
  }
}

응답

{
  "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 병합 패치 의미론을 사용하여 부분 업데이트를 적용합니다.

PATCH /admin/config/{section}

요청 본문

{
  "config": {
    "level": "warn"
  }
}

지정된 필드만 업데이트되고 다른 필드는 변경되지 않습니다.

응답

{
  "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
    }
  }'

설정 검증

변경 사항을 적용하지 않고 설정을 검증합니다.

POST /admin/config/validate

요청 본문

{
  "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
    }
  }'

설정 적용

보류 중인 설정 변경 사항을 즉시 적용합니다 (핫 리로드 트리거).

POST /admin/config/apply

요청 본문

{
  "sections": ["logging", "rate_limiting"],
  "force": false
}
필드 타입 필수 설명
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

설정 내보내기

지정된 형식으로 현재 설정을 내보냅니다.

POST /admin/config/export

요청 본문

{
  "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"]
  }'

설정 가져오기

콘텐츠에서 설정을 가져오고 적용합니다.

POST /admin/config/import

요청 본문

{
  "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
  }'

설정 히스토리 가져오기

설정 변경 히스토리를 확인합니다.

GET /admin/config/history

쿼리 파라미터

파라미터 타입 필수 설명
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

설정 롤백

이전 설정 버전으로 롤백합니다.

POST /admin/config/rollback/{version}

경로 파라미터

파라미터 타입 필수 설명
version integer 롤백할 버전 번호

요청 본문

{
  "sections": ["logging", "rate_limiting"],
  "dry_run": false
}
필드 타입 필수 설명
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

백엔드 추가

새 백엔드를 동적으로 추가합니다.

POST /admin/backends

요청 본문

{
  "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"]
  }'

백엔드 가져오기

특정 백엔드의 설정을 가져옵니다.

GET /admin/backends/{name}

응답

{
  "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"
  }
}

예제

curl -s http://localhost:8080/admin/backends/openai \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

백엔드 업데이트

백엔드 설정을 업데이트합니다.

PUT /admin/backends/{name}

요청 본문

{
  "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"]
  }'

백엔드 삭제

라우터에서 백엔드를 제거합니다.

DELETE /admin/backends/{name}

쿼리 파라미터

파라미터 타입 필수 설명
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"

백엔드 가중치 업데이트

로드 밸런싱을 위한 백엔드 가중치만 업데이트합니다.

PUT /admin/backends/{name}/weight

요청 본문

{
  "weight": 5
}

응답

{
  "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}'

백엔드 모델 업데이트

백엔드의 모델 목록을 업데이트합니다.

PUT /admin/backends/{name}/models

요청 본문

{
  "models": ["gpt-4", "gpt-4-turbo", "gpt-4o", "gpt-3.5-turbo"],
  "append": false
}
필드 타입 필수 설명
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 설정은 핫 리로드됩니다. permissiveblocking 사이를 바꿔도 재시작 없이 적용됩니다.

영속화와 핫 리로드

이 엔드포인트로 만들거나 고친 키는 인메모리 키 저장소에 있습니다. api_keys.persistence_file을 지정하면 런타임 변경이 그 파일에 기록되고(틸드 확장 지원) 다음 시작 때 복원되므로, 관리자가 만든 키가 재시작 후에도 남습니다. persistence_file이 없으면 런타임 키는 인메모리 전용이라 재시작 시 사라집니다. 인라인 설정이나 api_keys_file에서 읽은 키는 읽기 전용 소스이며 설정 핫 리로드 때 다시 적재됩니다.

API 키 목록 조회

GET /admin/api-keys

모든 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
  }
}

예제

curl -s http://localhost:8080/admin/api-keys \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

API 키 생성

POST /admin/api-keys

새 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 키 조회

GET /admin/api-keys/{id}

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 키 수정

PUT /admin/api-keys/{id}

기존 키의 속성을 하나 이상 수정합니다. 본문에 있는 필드만 바뀌고 생략한 필드는 그대로 둡니다. 키 값 자체는 이 엔드포인트로 바뀌지 않습니다(그 용도는 교체).

요청 본문

{
  "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 키 삭제

DELETE /admin/api-keys/{id}

키를 영구히 폐기하고 제거합니다. 삭제 후에는 옛 값을 제시하는 클라이언트의 인증이 실패합니다. 이 작업은 되돌릴 수 없습니다.

응답

{
  "success": true,
  "action": "delete",
  "id": "key-acme-1"
}

오류 응답

  • 404 Not Found: 해당 id의 키가 없는 경우.

예제

curl -X DELETE http://localhost:8080/admin/api-keys/key-acme-1 \
  -H "Authorization: Bearer $ADMIN_TOKEN"

API 키 교체

POST /admin/api-keys/{id}/rotate

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 키 활성화

POST /admin/api-keys/{id}/enable

키를 활성 상태로 표시합니다. 다시 활성화한 키는 만료되지 않았다면 또 인증됩니다.

응답

{
  "success": true,
  "action": "enable",
  "id": "key-acme-1"
}

오류 응답

  • 404 Not Found: 해당 id의 키가 없는 경우.

예제

curl -X POST http://localhost:8080/admin/api-keys/key-acme-1/enable \
  -H "Authorization: Bearer $ADMIN_TOKEN"

API 키 비활성화

POST /admin/api-keys/{id}/disable

키를 삭제하지 않고 비활성 상태로 표시합니다. 비활성 키는 인증에 실패하지만 설정은 그대로 유지되므로 나중에 다시 활성화할 수 있습니다. 삭제 대신 되돌릴 수 있는 일시 정지가 필요할 때 씁니다.

응답

{
  "success": true,
  "action": "disable",
  "id": "key-acme-1"
}

오류 응답

  • 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.yamladmin.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_windowtoken_tracking 설정은 핫 리로드를 지원합니다: 재시작 없이 즉시 변경 사항이 적용됩니다.

통계 영속화

persistence 하위 섹션이 존재하고 enabledtrue이면, 라우터는 주기적으로 통계 스냅샷을 디스크에 저장하고 시작 시 복원합니다. 따라서 요청 카운터, 모델별 분류, 레이턴시 링 버퍼가 재시작 후에도 유지됩니다.

동작 방식:

  • 시작 시 라우터가 스냅샷 파일을 읽고 모든 카운터와 링 버퍼 레코드를 복원합니다. 업타임은 각 재시작마다 초기화됩니다.
  • 백그라운드 태스크가 snapshot_interval마다 새 스냅샷을 저장합니다. 손상 방지를 위해 원자적 쓰기(임시 파일 + 이름 변경)를 사용합니다.
  • 정상 종료(SIGTERM/SIGINT) 시, 프로세스 종료 전에 최종 스냅샷이 저장됩니다.
  • 스냅샷 파일이 없거나, 손상되었거나, max_age보다 오래된 경우 라우터는 새 카운터로 시작하고 경고 또는 정보 메시지를 로깅합니다.

snapshot_intervalmax_age지원되는 기간 형식:

형식 예제 의미
Xs 30s 30초
Xm 5m 5분
Xh 1h 1시간
Xd 7d 7일

max_age"0" 또는 ""으로 설정하면 유효 기간 검사를 비활성화합니다 (기간에 관계없이 항상 복원).

전체 통계 조회

GET /admin/stats

전체, 모델별, 백엔드별 통계를 반환합니다.

쿼리 파라미터

파라미터 타입 설명
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

모델별 통계 조회

GET /admin/stats/models

모델별 분류만 반환합니다 (전체 통계 응답의 부분집합).

응답

{
  "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'

백엔드별 통계 조회

GET /admin/stats/backends

백엔드별 분류만 반환합니다. 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 기준 내림차순으로 정렬됩니다.

예제

curl -s http://localhost:8080/admin/stats/backends \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

API 키별·사용자별 통계

이 엔드포인트들은 각 요청을 인증한 API 키와 그 키에 연결된 사용자를 기준으로 사용량을 나눕니다. 모델별 통계 조회, 백엔드별 통계 조회와 나란히 놓이며, 같은 컬렉터를 쓰되 묶는 기준만 다릅니다.

식별자와 버킷 규칙

  • 집계 범위: 모든 추론 표면이 이 통계에 기여합니다 — /v1/chat/completions, /anthropic/v1/messages, 그리고 OpenAI Responses API(/v1/responses, 패스스루·Chat Completions 변환·Anthropic 변환 전략 포함). 성공한 비스트리밍 요청은 전체 토큰 사용량을 담고, 스트리밍 요청은 연결 시점에 기록됩니다(요청 수와 API 키별·사용자별 귀속은 기록하되, 토큰 합계는 스트림 종료 시에만 알 수 있어 생략).
  • api_key_id는 원본 키가 아니라 역산 불가능한 파생 식별자입니다. api_key_id Prometheus 라벨과 같은 값이며, 발급된 키의 id에 대응합니다. 사용자별 엔드포인트는 일치한 키에 연결된 user_id를 기준으로 묶습니다. 파생 api_key_idmetrics 피처가 컴파일되어 있어야 하며, 없으면 API 키별 귀속이 "anonymous" 버킷으로 합쳐집니다(사용자별 귀속은 키의 user_id를 직접 읽으므로 영향받지 않습니다).
  • 키가 없는(또는 연결된 사용자가 없는) 요청은 "anonymous" 버킷으로 들어갑니다.
  • 각 차원은 서로 다른 식별자 1000개(예약 버킷 제외)의 카디널리티 상한을 둡니다. 상한에 도달하면 그 뒤의 새 식별자는 "unknown" 오버플로 버킷으로 접혀 들어가, 사용량이 합계로는 계속 집계됩니다.
  • window 쿼리 파라미터는 GET /admin/stats와의 일관성을 위해 받아들여 응답에 그대로 돌려주지만, API 키별·사용자별 집계는 GET /admin/stats/models와 똑같이 전체 기간 합계입니다. 식별자는 요청 핫 패스 밖에서 결정되므로, 시간 필터 레이턴시 백분위에 쓰는 윈도우 링 버퍼 레코드에는 들어 있지 않습니다.

ApiKeyStatsUserStats 객체는 같은 모양입니다.

필드 타입 설명
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 키별 통계 조회

GET /admin/stats/api-keys

추적 중인 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)

GET /admin/stats/api-keys/{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

사용자별 통계 조회

GET /admin/stats/users

추적 중인 사용자 식별자(일치한 키에 연결된 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"
    }
  ]
}
예제
curl -s http://localhost:8080/admin/stats/users \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

사용자별 통계 조회 (ID)

GET /admin/stats/users/{user_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 키별 모델 분류 조회

GET /admin/stats/api-keys/{id}/models

단일 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

사용자별 모델 분류 조회

GET /admin/stats/users/{user_id}/models

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 키별 일별 사용량 시계열 조회

GET /admin/stats/api-keys/{id}/series?from=&to=&interval=day

단일 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

사용자별 일별 사용량 시계열 조회

GET /admin/stats/users/{user_id}/series?from=&to=&interval=day

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

통계 초기화

POST /admin/stats/reset

모든 카운터, 모델별 레코드, 백엔드별 레코드, API 키별·사용자별 레코드(모델별 분류 및 일별 시계열 버킷 포함), 레이턴시 링 버퍼를 초기화합니다. 이 작업은 되돌릴 수 없습니다.

응답

{
  "success": true,
  "action": "reset",
  "message": "Statistics counters have been reset"
}

예제

curl -X POST http://localhost:8080/admin/stats/reset \
  -H "Authorization: Bearer $ADMIN_TOKEN"

지속 메트릭 로그 API

지속 메트릭 로그 API는 로컬 저장소(기본값 SQLite)에 보존된 최근 Prometheus 레지스트리 이력을 노출합니다. 저장소 레이아웃과 보존 기간 산정, 설정 옵션은 지속 메트릭 로그 가이드를 참고하세요.

메트릭 이력 조회

GET /admin/metrics/history?metric=<name>&from=<ts>&to=<ts>&limit=<n>

[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 Requestmetric이 누락·과대 크기이거나 시간 범위가 0 이하인 경우입니다.
  • 404 Not Found — 지속 기능이 비활성화된 경우입니다(metrics.persistence.enabled: false).
  • 500 Internal Server Error — 저장소 오류입니다.
  • 503 Service Unavailablemetrics-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 섹션에서 구성합니다. 전체 설정 세부 사항은 응답 캐시 설정 가이드를 참조하세요.

응답 캐시 통계 조회

GET /admin/response-cache/stats

히트/미스 카운트, 메모리 사용량, 설정 요약을 포함한 현재 응답 캐시 통계를 반환합니다.

응답

{
  "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 또는 섹션이 없는 경우), enabledfalse, entriescapacity0, confignull입니다.

응답 필드

필드 타입 설명
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

응답 캐시 무효화

POST /admin/response-cache/invalidate

캐시 항목을 삭제합니다. clear_all: true를 통한 전체 캐시 무효화만 지원하며, 모델 또는 테넌트별 대상 무효화는 제공되지 않습니다.

요청 본문

{
  "clear_all": true,
  "model": "gpt-4",
  "tenant_id": "tenant-abc"
}
필드 타입 필수 설명
clear_all boolean 아니오 true이면 전체 캐시를 삭제합니다. 기본값: false.
model string 아니오 받기는 하지만 현재 무시됩니다. clear_all만 처리됩니다. 256자를 초과할 수 없습니다.
tenant_id string 아니오 받기는 하지만 현재 무시됩니다. clear_all만 처리됩니다. 256자를 초과할 수 없습니다.

응답 (clear_all: true)

{
  "success": true,
  "action": "clear_all",
  "cleared_entries": 42
}

응답 (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."
}

응답 (캐시 비활성화)

{
  "success": false,
  "error": "Response cache is not enabled"
}

예제

# 전체 캐시 삭제
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 캐시 인덱스 통계 조회

GET /admin/kv-index/stats

인덱스 크기, 이벤트 소스 연결 상태, 라우팅 결정 카운트를 포함한 전체 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 또는 섹션이 없는 경우), enabledfalse, confignull, 모든 카운터는 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 총 라우팅 결정 수

예제

curl -s http://localhost:8080/admin/kv-index/stats \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

백엔드별 KV 캐시 상태 조회

GET /admin/kv-index/backends

백엔드별 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: falseconfigured_endpoint 필드와 함께 포함됩니다.

응답 (비활성화)

{
  "enabled": false,
  "backends": []
}

응답 필드

필드 타입 설명
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 캐시 인덱스 삭제

POST /admin/kv-index/clear

KV 캐시 인덱스의 모든 항목을 삭제합니다. 디버깅 및 테스트용입니다. 프로덕션에서는 수신되는 KV 이벤트로부터 인덱스가 자동으로 재구축됩니다.

응답 (성공)

{
  "success": true,
  "entries_before_clear": 120,
  "cleared_entries": 45
}

entries_before_clear는 삭제 전 총 (접두사, 백엔드) 쌍 수입니다. cleared_entries는 제거된 접두사 해시 버킷 수입니다. Redis 백엔드의 경우, cleared_entries는 삭제된 Redis 키 수를 카운트합니다; 각 키에 TTL이 있으므로 남은 키는 자동으로 만료됩니다.

응답 (비활성화)

{
  "success": false,
  "error": "KV cache index is not enabled"
}

예제

curl -X POST http://localhost:8080/admin/kv-index/clear \
  -H "Authorization: Bearer $ADMIN_TOKEN"

스마트 라우팅 Admin API

스마트 라우팅 Admin API는 모델 티어 레지스트리를 노출하며, 라우터가 각 모델에 할당하는 티어와 도메인 프로필을 조회하고 재시작 없이 런타임에서 업데이트할 수 있습니다. 모든 엔드포인트는 /admin/smart-routing 아래에 마운트되며 동일한 인증을 요구합니다.

스마트 라우팅은 YAML 설정에서 smart_routing.enabled: true로 활성화합니다. 비활성화 상태에서도 목록 엔드포인트는 "enabled": false를 반환하며 빈 프로필 목록을 돌려줍니다.

모델 프로필 목록 조회

GET /admin/smart-routing/model-profiles

명시적으로 설정된 프로필과 시작 이후 캐시된 자동 추론 프로필을 모두 반환합니다.

응답

{
  "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"

모델 프로필 단건 조회

GET /admin/smart-routing/model-profiles/{model}

특정 모델의 해석된 프로필을 반환합니다. 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"

모델 프로필 업데이트

PUT /admin/smart-routing/model-profiles

모든 모델 프로필 설정을 교체합니다. 레지스트리는 즉시 리로드되며 추론 캐시가 초기화되어 이후 요청은 새 프로필을 기준으로 재평가됩니다.

요청 본문

{
  "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 아니오 프로필 매칭 실패 시 폴백 티어

응답

{
  "status": "updated",
  "profiles_count": 2,
  "default_tier": 2
}

예제

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"]}
    ]
  }'

스마트 라우팅 상태

GET /admin/smart-routing/status

활성화 상태, 부하 상태, 분류기 방식, 정책 수 등 스마트 라우팅 전반의 상태를 반환합니다.

응답

{
  "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
}

스마트 라우팅 통계

GET /admin/smart-routing/stats

프로필 수, 정책 수, LLM 분류기 캐시 정보 등 집계된 라우팅 통계를 반환합니다.

분류 (진단용)

POST /admin/smart-routing/classify

실제 라우팅 없이 요청을 분류합니다. 분류 동작을 디버깅할 때 유용합니다.

요청

{
  "payload": {
    "model": "auto",
    "messages": [{"role": "user", "content": "Hello, world!"}]
  }
}

응답

{
  "complexity": "trivial",
  "domain": "general",
  "confidence": 0.95,
  "classifier_type": "rule_based",
  "required_capabilities": [],
  "reasoning": null,
  "signals": [
    {"name": "message_length", "strength": 0.1, "influences": "complexity"}
  ]
}

시뮬레이션 (진단용)

POST /admin/smart-routing/simulate

실제 요청 전달 없이 전체 라우팅 파이프라인(분류 + 정책 평가 + 모델 선택 + 부하 상태)을 시뮬레이션합니다. 전체 라우팅 결정 과정을 반환합니다.

요청

분류 엔드포인트와 동일합니다.

응답

{
  "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
}

라우팅 정책 목록 조회

GET /admin/smart-routing/policies

현재 활성화된 라우팅 정책과 해당 조건 및 대상을 반환합니다.

라우팅 정책 업데이트

PUT /admin/smart-routing/policies

런타임에 라우팅 정책을 핫 리로드합니다.

요청

{
  "routing_policies": [
    {
      "name": "all_to_flagship",
      "when": {},
      "route_to": {"tier": 1}
    }
  ],
  "virtual_model": "auto",
  "intercept_all": false
}

부하 상태

GET /admin/smart-routing/load-state

평가 정보를 포함한 현재 부하 상태를 반환합니다.

응답

{
  "enabled": true,
  "state": "normal",
  "max_tier": null,
  "prefer_quantized": false,
  "reject_expert": false
}

캐시 통계

GET /admin/smart-routing/cache/stats

LLM 분류기 캐시 통계를 반환합니다.

응답

{
  "available": true,
  "entries": 42,
  "capacity": 10000,
  "ttl_seconds": 300
}

캐시 초기화

POST /admin/smart-routing/cache/clear

LLM 분류기 캐시의 모든 항목을 삭제합니다.

응답

{
  "status": "cleared",
  "entries_removed": 42
}

가드레일 Admin API

가드레일 Admin API는 콘텐츠 안전 가드레일 정책을 재시작 없이 런타임에서 조회하고 조정할 수 있게 합니다. 변경 사항은 실행 중인 GuardrailService가 구독하는 핫 리로드 설정 채널을 통해 전파되므로, 모드 전환, 활성화 토글, 임곗값 변경, 라우트 오버라이드가 라이브 요청 경로에 즉시 반영됩니다. 모든 엔드포인트는 /admin/guardrails 아래에 마운트되며 나머지 Admin API와 동일한 인증 및 감사 로깅을 요구합니다.

가드레일 프로바이더 집합 자체는 설정 파일에서 정의합니다. 이 엔드포인트들은 기존 프로바이더와 글로벌/라우트별 정책을 토글하고 조정할 뿐, 프로바이더를 생성하거나 제거하지 않습니다.

가드레일 정책 조회

GET /admin/guardrails

현재 적용 중인 가드레일 정책과 상태 요약을 반환합니다. 비밀값(bypass_api_keys 목록)은 마스킹됩니다. 시작 시 가드레일이 비활성화되어 실행 중인 서비스가 없으면 service_activefalse이며, 이 경우 반환되는 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": [] }
  }
}

예제

curl http://localhost:8080/admin/guardrails \
  -H "Authorization: Bearer <admin-token>"

가드레일 정책 업데이트

PATCH /admin/guardrails

글로벌 가드레일 정책을 부분 업데이트합니다. 모든 필드는 선택 사항이며, 제공된 필드만 변경됩니다. 프로바이더와 라우트별 오버라이드는 아래의 전용 엔드포인트로 관리합니다. 후보 정책은 적용 전에 검증되며, 잘못된 변경(예: 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 키 목록 교체

응답

{
  "status": "updated",
  "enabled": true,
  "mode": "enforce"
}

예제

curl -X PATCH http://localhost:8080/admin/guardrails \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"mode": "enforce"}'

가드레일 프로바이더 업데이트

PUT /admin/guardrails/providers/{name}

설정된 단일 프로바이더의 런타임 설정을 업데이트합니다. 모든 필드는 선택 사항입니다. 주어진 이름의 프로바이더가 설정되어 있지 않으면 404를 반환합니다.

요청 본문

필드 타입 설명
enabled boolean 이 프로바이더를 활성화 또는 비활성화
category_thresholds object 카테고리별 점수 임곗값 교체({ "violence": 0.8 })
timeout_ms integer 또는 null 프로바이더별 타임아웃 오버라이드 설정 또는 해제
on_error string 또는 null 프로바이더별 오류 정책 오버라이드 설정 또는 해제

응답

{
  "status": "updated",
  "provider": "llama-guard",
  "enabled": false
}

예제

curl -X PUT http://localhost:8080/admin/guardrails/providers/llama-guard \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

가드레일 라우트 오버라이드 설정

PUT /admin/guardrails/routes/{route}

주어진 라우트에 대한 라우트별 가드레일 오버라이드를 생성하거나 교체합니다. 요청 본문은 라우트 오버라이드 객체이며, 생략된 필드는 글로벌 정책을 상속합니다.

요청 본문

필드 타입 설명
mode string 이 라우트의 동작 모드 오버라이드
enabled boolean 이 라우트에서 가드레일 실행 여부 오버라이드
providers array 이 라우트를 프로바이더 이름의 부분 집합으로 제한
category_thresholds object 라우트별 카테고리 임곗값
allow object 라우트 전용 허용 목록
deny object 라우트 전용 차단 목록

응답

{
  "status": "updated",
  "route": "gpt-4o"
}

예제

curl -X PUT http://localhost:8080/admin/guardrails/routes/gpt-4o \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"mode": "monitor"}'

가드레일 라우트 오버라이드 삭제

DELETE /admin/guardrails/routes/{route}

라우트별 오버라이드를 제거하여 해당 라우트를 글로벌 정책으로 되돌립니다. 해당 라우트에 오버라이드가 설정되어 있지 않으면 404를 반환합니다.

응답

{
  "status": "deleted",
  "route": "gpt-4o"
}

가드레일 테스트 (드라이런)

POST /admin/guardrails/test

임곗값 튜닝을 위한 진단 엔드포인트입니다. 등록된 모든 프로바이더를 제공된 샘플 텍스트에 대해 실행하고, 각 프로바이더의 판정과 집계된 최고 심각도 우선(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'

오류 처리

오류 응답 형식

{
  "error_code": "string",
  "message": "string",
  "details": {}
}

오류 코드

코드 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 엔드포인트 속도 제한을 고려하세요:

admin:
  rate_limit:
    requests_per_minute: 60
    burst: 10

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 파일에 저장된 시스템 프롬프트를 관리할 수 있습니다. 메인 설정 파일을 수정하지 않고 시스템 프롬프트를 중앙에서 관리할 수 있습니다.

모든 프롬프트 목록

소스 및 콘텐츠와 함께 설정된 모든 프롬프트 목록을 가져옵니다.

GET /admin/config/prompts

응답

{
  "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"
}

예제

curl -s http://localhost:8080/admin/config/prompts \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

프롬프트 파일 가져오기

특정 프롬프트 파일의 콘텐츠를 가져옵니다.

GET /admin/config/prompts/{path}

경로 파라미터

파라미터 타입 필수 설명
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

프롬프트 파일 업데이트

새 콘텐츠로 프롬프트 파일을 생성하거나 업데이트합니다.

PUT /admin/config/prompts/{path}

요청 본문

{
  "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."
  }'

프롬프트 파일 리로드

디스크에서 모든 프롬프트 파일을 리로드합니다. 수동 파일 편집 후 유용합니다.

POST /admin/config/prompts/reload

응답

{
  "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"