콘텐츠로 이동

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는 StatsCollector가 수집한 집계 요청 메트릭을 노출합니다. 네 가지 엔드포인트 모두 /admin/stats 하위에 마운트되며 나머지 Admin API와 동일한 인증을 공유합니다.

통계 수집은 기본적으로 활성화되어 있습니다. config.yamladmin.stats 섹션을 통해 설정하거나 비활성화할 수 있습니다:

admin:
  stats:
    enabled: true                # 수집 활성화/비활성화 (기본값: true)
    retention_window: 24h        # 윈도우 쿼리용 링 버퍼 보존 기간 (기본값: 24h)
    token_tracking: true         # 토큰 사용량 파싱 (기본값: true)
    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

통계 초기화

POST /admin/stats/reset

모든 카운터, 모델별 레코드, 백엔드별 레코드, 레이턴시 링 버퍼를 초기화합니다. 이 작업은 되돌릴 수 없습니다.

응답

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

예제

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

응답 캐시 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 아니오 향후 대상 무효화를 위해 예약됨. 256자를 초과할 수 없습니다.
tenant_id string 아니오 향후 대상 무효화를 위해 예약됨. 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"

데이터 모델

설정 섹션

섹션 설명 핫 리로드
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 모델 라우팅 규칙 점진적

백엔드 객체

{
  "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, ratelimiting, circuitbreaker, retry, globalprompts, apikeys
점진적 기존 연결 유지, 새 연결이 새 설정 사용 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 '{}'